From f7bf91410824e9090677943d51c46eed290bf736 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 1 Jun 2019 13:50:15 -0700 Subject: [PATCH 01/78] Fix `pytest.mark.parametrize` when the argvalue is an iterator --- changelog/5354.bugfix.rst | 1 + src/_pytest/mark/structures.py | 10 +++++++--- testing/test_mark.py | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 changelog/5354.bugfix.rst diff --git a/changelog/5354.bugfix.rst b/changelog/5354.bugfix.rst new file mode 100644 index 00000000000..812ea8364aa --- /dev/null +++ b/changelog/5354.bugfix.rst @@ -0,0 +1 @@ +Fix ``pytest.mark.parametrize`` when the argvalues is an iterator. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 561ccc3f45d..9602e8acfa3 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -113,14 +113,18 @@ def _parse_parametrize_args(argnames, argvalues, **_): force_tuple = len(argnames) == 1 else: force_tuple = False - parameters = [ + return argnames, force_tuple + + @staticmethod + def _parse_parametrize_parameters(argvalues, force_tuple): + return [ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ] - return argnames, parameters @classmethod def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): - argnames, parameters = cls._parse_parametrize_args(argnames, argvalues) + argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) + parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) del argvalues if parameters: diff --git a/testing/test_mark.py b/testing/test_mark.py index 5bd97d547e8..dd9d352309d 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -413,6 +413,28 @@ def test_func(a, b): assert result.ret == 0 +def test_parametrize_iterator(testdir): + """parametrize should work with generators (#5354).""" + py_file = testdir.makepyfile( + """\ + import pytest + + def gen(): + yield 1 + yield 2 + yield 3 + + @pytest.mark.parametrize('a', gen()) + def test(a): + assert a >= 1 + """ + ) + result = testdir.runpytest(py_file) + assert result.ret == 0 + # should not skip any tests + result.stdout.fnmatch_lines(["*3 passed*"]) + + class TestFunctional(object): def test_merging_markers_deep(self, testdir): # issue 199 - propagate markers into nested classes From f078984c2e2775f9c47f43f248c366d638213b76 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 2 Jun 2019 12:51:43 -0300 Subject: [PATCH 02/78] Fix all() unroll for non-generators/non-list comprehensions (#5360) Fix all() unroll for non-generators/non-list comprehensions --- changelog/5358.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 16 ++++++++++++---- testing/test_assertrewrite.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 changelog/5358.bugfix.rst diff --git a/changelog/5358.bugfix.rst b/changelog/5358.bugfix.rst new file mode 100644 index 00000000000..181da1e0ec2 --- /dev/null +++ b/changelog/5358.bugfix.rst @@ -0,0 +1 @@ +Fix assertion rewriting of ``all()`` calls to deal with non-generators. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 5e2c5397bb8..b9142147b12 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -949,11 +949,21 @@ def visit_BinOp(self, binop): res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) return res, explanation + @staticmethod + def _is_any_call_with_generator_or_list_comprehension(call): + """Return True if the Call node is an 'any' call with a generator or list comprehension""" + return ( + isinstance(call.func, ast.Name) + and call.func.id == "all" + and len(call.args) == 1 + and isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)) + ) + def visit_Call_35(self, call): """ visit `ast.Call` nodes on Python3.5 and after """ - if isinstance(call.func, ast.Name) and call.func.id == "all": + if self._is_any_call_with_generator_or_list_comprehension(call): return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] @@ -980,8 +990,6 @@ def visit_Call_35(self, call): def _visit_all(self, call): """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] assertion_module = ast.Module( body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)] @@ -1009,7 +1017,7 @@ def visit_Call_legacy(self, call): """ visit `ast.Call nodes on 3.4 and below` """ - if isinstance(call.func, ast.Name) and call.func.id == "all": + if self._is_any_call_with_generator_or_list_comprehension(call): return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 19d050f8769..22f53ab9550 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -677,7 +677,7 @@ def __repr__(self): assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg - def test_unroll_generator(self, testdir): + def test_unroll_all_generator(self, testdir): testdir.makepyfile( """ def check_even(num): @@ -692,7 +692,7 @@ def test_generator(): result = testdir.runpytest() result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) - def test_unroll_list_comprehension(self, testdir): + def test_unroll_all_list_comprehension(self, testdir): testdir.makepyfile( """ def check_even(num): @@ -707,6 +707,31 @@ def test_list_comprehension(): result = testdir.runpytest() result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) + def test_unroll_all_object(self, testdir): + """all() for non generators/non list-comprehensions (#5358)""" + testdir.makepyfile( + """ + def test(): + assert all((1, 0)) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*assert False*", "*where False = all((1, 0))*"]) + + def test_unroll_all_starred(self, testdir): + """all() for non generators/non list-comprehensions (#5358)""" + testdir.makepyfile( + """ + def test(): + x = ((1, 0),) + assert all(*x) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + ["*assert False*", "*where False = all(*((1, 0),))*"] + ) + def test_for_loop(self, testdir): testdir.makepyfile( """ From abb853f482e5fa37b2f18f22662d33e52117e696 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sun, 2 Jun 2019 10:09:51 -0700 Subject: [PATCH 03/78] Preparing release version 4.6.1 --- CHANGELOG.rst | 12 ++++++++++++ changelog/5354.bugfix.rst | 1 - changelog/5358.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.1.rst | 19 +++++++++++++++++++ doc/en/example/simple.rst | 2 +- 6 files changed, 33 insertions(+), 3 deletions(-) delete mode 100644 changelog/5354.bugfix.rst delete mode 100644 changelog/5358.bugfix.rst create mode 100644 doc/en/announce/release-4.6.1.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 32267f4dd42..a2bf12d7ea2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,18 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.1 (2019-06-02) +========================= + +Bug Fixes +--------- + +- `#5354 `_: Fix ``pytest.mark.parametrize`` when the argvalues is an iterator. + + +- `#5358 `_: Fix assertion rewriting of ``all()`` calls to deal with non-generators. + + pytest 4.6.0 (2019-05-31) ========================= diff --git a/changelog/5354.bugfix.rst b/changelog/5354.bugfix.rst deleted file mode 100644 index 812ea8364aa..00000000000 --- a/changelog/5354.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``pytest.mark.parametrize`` when the argvalues is an iterator. diff --git a/changelog/5358.bugfix.rst b/changelog/5358.bugfix.rst deleted file mode 100644 index 181da1e0ec2..00000000000 --- a/changelog/5358.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix assertion rewriting of ``all()`` calls to deal with non-generators. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index fa53441ce59..2c05e2e59cb 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.1 release-4.6.0 release-4.5.0 release-4.4.2 diff --git a/doc/en/announce/release-4.6.1.rst b/doc/en/announce/release-4.6.1.rst new file mode 100644 index 00000000000..78d017544d2 --- /dev/null +++ b/doc/en/announce/release-4.6.1.rst @@ -0,0 +1,19 @@ +pytest-4.6.1 +======================================= + +pytest 4.6.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 140f4b840f1..cf8298ddc2a 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -441,7 +441,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 - 0.20s call test_some_are_slow.py::test_funcslow1 + 0.21s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= From 12bf458719cf29734f200094819fbedd8acc3464 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Jun 2019 09:18:47 -0700 Subject: [PATCH 04/78] Merge pull request #5373 from asottile/revert_all_handling Revert unrolling of `all()` --- changelog/5370.bugfix.rst | 1 + changelog/5371.bugfix.rst | 1 + changelog/5372.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 33 -------------- testing/test_assertrewrite.py | 78 -------------------------------- 5 files changed, 3 insertions(+), 111 deletions(-) create mode 100644 changelog/5370.bugfix.rst create mode 100644 changelog/5371.bugfix.rst create mode 100644 changelog/5372.bugfix.rst diff --git a/changelog/5370.bugfix.rst b/changelog/5370.bugfix.rst new file mode 100644 index 00000000000..70def0d270a --- /dev/null +++ b/changelog/5370.bugfix.rst @@ -0,0 +1 @@ +Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions. diff --git a/changelog/5371.bugfix.rst b/changelog/5371.bugfix.rst new file mode 100644 index 00000000000..46ff5c89047 --- /dev/null +++ b/changelog/5371.bugfix.rst @@ -0,0 +1 @@ +Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``. diff --git a/changelog/5372.bugfix.rst b/changelog/5372.bugfix.rst new file mode 100644 index 00000000000..e9b644db290 --- /dev/null +++ b/changelog/5372.bugfix.rst @@ -0,0 +1 @@ +Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index b9142147b12..1c6161b212d 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -949,22 +949,10 @@ def visit_BinOp(self, binop): res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) return res, explanation - @staticmethod - def _is_any_call_with_generator_or_list_comprehension(call): - """Return True if the Call node is an 'any' call with a generator or list comprehension""" - return ( - isinstance(call.func, ast.Name) - and call.func.id == "all" - and len(call.args) == 1 - and isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)) - ) - def visit_Call_35(self, call): """ visit `ast.Call` nodes on Python3.5 and after """ - if self._is_any_call_with_generator_or_list_comprehension(call): - return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] @@ -988,25 +976,6 @@ def visit_Call_35(self, call): 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 #5062""" - gen_exp = call.args[0] - assertion_module = ast.Module( - body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)] - ) - 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, - body=assertion_module.body, - orelse=[], - ) - 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 res, expl = self.visit(starred.value) @@ -1017,8 +986,6 @@ def visit_Call_legacy(self, call): """ visit `ast.Call nodes on 3.4 and below` """ - if self._is_any_call_with_generator_or_list_comprehension(call): - return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 22f53ab9550..87dada213d6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -656,12 +656,6 @@ def __repr__(self): else: assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] - def test_unroll_expression(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): @@ -677,78 +671,6 @@ def __repr__(self): assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg - def test_unroll_all_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_unroll_all_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_unroll_all_object(self, testdir): - """all() for non generators/non list-comprehensions (#5358)""" - testdir.makepyfile( - """ - def test(): - assert all((1, 0)) - """ - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*assert False*", "*where False = all((1, 0))*"]) - - def test_unroll_all_starred(self, testdir): - """all() for non generators/non list-comprehensions (#5358)""" - testdir.makepyfile( - """ - def test(): - x = ((1, 0),) - assert all(*x) - """ - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines( - ["*assert False*", "*where False = all(*((1, 0),))*"] - ) - - 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 70f0b77c729d689d79498bcb525a8ec6d5464dd0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 3 Jun 2019 10:43:09 -0700 Subject: [PATCH 05/78] Preparing release version 4.6.2 --- CHANGELOG.rst | 15 +++++++++++++++ changelog/5370.bugfix.rst | 1 - changelog/5371.bugfix.rst | 1 - changelog/5372.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.2.rst | 18 ++++++++++++++++++ doc/en/example/simple.rst | 2 +- 7 files changed, 35 insertions(+), 4 deletions(-) delete mode 100644 changelog/5370.bugfix.rst delete mode 100644 changelog/5371.bugfix.rst delete mode 100644 changelog/5372.bugfix.rst create mode 100644 doc/en/announce/release-4.6.2.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a2bf12d7ea2..715238b327b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,21 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.2 (2019-06-03) +========================= + +Bug Fixes +--------- + +- `#5370 `_: Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions. + + +- `#5371 `_: Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``. + + +- `#5372 `_: Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression. + + pytest 4.6.1 (2019-06-02) ========================= diff --git a/changelog/5370.bugfix.rst b/changelog/5370.bugfix.rst deleted file mode 100644 index 70def0d270a..00000000000 --- a/changelog/5370.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert unrolling of ``all()`` to fix ``NameError`` on nested comprehensions. diff --git a/changelog/5371.bugfix.rst b/changelog/5371.bugfix.rst deleted file mode 100644 index 46ff5c89047..00000000000 --- a/changelog/5371.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert unrolling of ``all()`` to fix incorrect handling of generators with ``if``. diff --git a/changelog/5372.bugfix.rst b/changelog/5372.bugfix.rst deleted file mode 100644 index e9b644db290..00000000000 --- a/changelog/5372.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert unrolling of ``all()`` to fix incorrect assertion when using ``all()`` in an expression. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 2c05e2e59cb..9379ae5b1f8 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.2 release-4.6.1 release-4.6.0 release-4.5.0 diff --git a/doc/en/announce/release-4.6.2.rst b/doc/en/announce/release-4.6.2.rst new file mode 100644 index 00000000000..8526579b9e7 --- /dev/null +++ b/doc/en/announce/release-4.6.2.rst @@ -0,0 +1,18 @@ +pytest-4.6.2 +======================================= + +pytest 4.6.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index cf8298ddc2a..140f4b840f1 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -441,7 +441,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 - 0.21s call test_some_are_slow.py::test_funcslow1 + 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= From 829a5986e8a0b6f3a8ad668e59736e11e0050ad5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 3 Jun 2019 22:18:17 -0300 Subject: [PATCH 06/78] Remove --recreate from .travis.yml (#5384) Remove --recreate from .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9914800f808..240d6cdf0f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -130,7 +130,7 @@ before_script: export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi -script: tox --recreate +script: tox after_success: - | From 92432ac45cccbd83fd16324932f96c23282f6b2c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 4 Jun 2019 17:48:48 -0700 Subject: [PATCH 07/78] Merge pull request #5393 from nicoddemus/unittest-self-5390 item.obj is again a bound method on TestCase function items --- changelog/5390.bugfix.rst | 1 + src/_pytest/unittest.py | 2 ++ testing/test_unittest.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 changelog/5390.bugfix.rst diff --git a/changelog/5390.bugfix.rst b/changelog/5390.bugfix.rst new file mode 100644 index 00000000000..3f57c3043d5 --- /dev/null +++ b/changelog/5390.bugfix.rst @@ -0,0 +1 @@ +Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index e2e7efdc539..3ff6f45d8db 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -114,6 +114,7 @@ class TestCaseFunction(Function): def setup(self): self._testcase = self.parent.obj(self.name) self._fix_unittest_skip_decorator() + self._obj = getattr(self._testcase, self.name) if hasattr(self, "_request"): self._request._fillfixtures() @@ -132,6 +133,7 @@ def _fix_unittest_skip_decorator(self): def teardown(self): self._testcase = None + self._obj = None def startTest(self, testcase): pass diff --git a/testing/test_unittest.py b/testing/test_unittest.py index bb41952abbe..e9982b3d5b9 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -144,6 +144,29 @@ def test_func2(self): reprec.assertoutcome(passed=2) +def test_function_item_obj_is_instance(testdir): + """item.obj should be a bound method on unittest.TestCase function items (#5390).""" + testdir.makeconftest( + """ + def pytest_runtest_makereport(item, call): + if call.when == 'call': + class_ = item.parent.obj + assert isinstance(item.obj.__self__, class_) + """ + ) + testdir.makepyfile( + """ + import unittest + + class Test(unittest.TestCase): + def test_foo(self): + pass + """ + ) + result = testdir.runpytest_inprocess() + result.stdout.fnmatch_lines(["* 1 passed in*"]) + + def test_teardown(testdir): testpath = testdir.makepyfile( """ From 0ae27714d1940cb012827faf56245c23cb40d113 Mon Sep 17 00:00:00 2001 From: Dirk Thomas Date: Tue, 4 Jun 2019 10:41:58 -0700 Subject: [PATCH 08/78] Backport of #5389: fix for 'files' = None in broken metadata --- changelog/5389.bugfix.rst | 1 + src/_pytest/config/__init__.py | 2 +- testing/test_config.py | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 changelog/5389.bugfix.rst diff --git a/changelog/5389.bugfix.rst b/changelog/5389.bugfix.rst new file mode 100644 index 00000000000..debf0a9da9d --- /dev/null +++ b/changelog/5389.bugfix.rst @@ -0,0 +1 @@ +Fix regressions of `#5063 `__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 7a5deb13f53..6d44bf3d773 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -800,7 +800,7 @@ def _mark_plugins_for_rewrite(self, hook): 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 file in dist.files or [] ) for name in _iter_rewritable_modules(package_files): diff --git a/testing/test_config.py b/testing/test_config.py index e588c262a9c..eefd908de59 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -586,6 +586,29 @@ def distributions(): testdir.parseconfig() +def test_importlib_metadata_broken_distribution(testdir, monkeypatch): + """Integration test for broken distributions with 'files' metadata being None (#5389)""" + monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) + + class DummyEntryPoint: + name = "mytestplugin" + group = "pytest11" + + def load(self): + return object() + + class Distribution: + version = "1.0" + files = None + entry_points = (DummyEntryPoint(),) + + def distributions(): + return (Distribution(),) + + monkeypatch.setattr(importlib_metadata, "distributions", distributions) + testdir.parseconfig() + + @pytest.mark.parametrize("block_it", [True, False]) def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it): monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) From 1b7597ac912e76aef2e3c09dd815cb4208dcf817 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 5 Jun 2019 11:21:44 +0200 Subject: [PATCH 09/78] [4.6] tests: restore tracing function Without this, `testing/test_pdb.py` (already without pexpect) will cause missing test coverage afterwards (for the same process). --- testing/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/testing/conftest.py b/testing/conftest.py index 6e01d710d11..627ee763d2f 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,6 +1,21 @@ # -*- coding: utf-8 -*- +import sys + import pytest +if sys.gettrace(): + + @pytest.fixture(autouse=True) + def restore_tracing(): + """Restore tracing function (when run with Coverage.py). + + https://bugs.python.org/issue37011 + """ + orig_trace = sys.gettrace() + yield + if sys.gettrace() != orig_trace: + sys.settrace(orig_trace) + @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems(config, items): From a93c50ccb96ad42ca47079c124e2e622296d363f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 5 Jun 2019 20:47:59 -0300 Subject: [PATCH 10/78] Fix verbosity bug in --collect-only (#5391) Fix verbosity bug in --collect-only --- changelog/5383.bugfix.rst | 2 ++ src/_pytest/logging.py | 13 +++++++--- testing/logging/test_reporting.py | 43 ++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 changelog/5383.bugfix.rst diff --git a/changelog/5383.bugfix.rst b/changelog/5383.bugfix.rst new file mode 100644 index 00000000000..53e25956d56 --- /dev/null +++ b/changelog/5383.bugfix.rst @@ -0,0 +1,2 @@ +``-q`` has again an impact on the style of the collected items +(``--collect-only``) when ``--log-cli-level`` is used. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 577a5407be0..2400737ee4e 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -424,10 +424,6 @@ def __init__(self, config): """ self._config = config - # enable verbose output automatically if live logging is enabled - if self._log_cli_enabled() and config.getoption("verbose") < 1: - config.option.verbose = 1 - self.print_logs = get_option_ini(config, "log_print") self.formatter = self._create_formatter( get_option_ini(config, "log_format"), @@ -644,6 +640,15 @@ def pytest_sessionstart(self): @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session): """Runs all collected test items.""" + + if session.config.option.collectonly: + yield + return + + if self._log_cli_enabled() and self._config.getoption("verbose") < 1: + # setting verbose flag is needed to avoid messy test progress output + self._config.option.verbose = 1 + with self.live_logs_context(): if self.log_file_handler is not None: with catching_logs(self.log_file_handler, level=self.log_file_level): diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index e7a3a80ebf1..6ff5ccfb55a 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -921,14 +921,45 @@ def test_collection_live_logging(testdir): result = testdir.runpytest("--log-cli-level=INFO") result.stdout.fnmatch_lines( - [ - "collecting*", - "*--- live log collection ---*", - "*Normal message*", - "collected 0 items", - ] + ["*--- live log collection ---*", "*Normal message*", "collected 0 items"] + ) + + +@pytest.mark.parametrize("verbose", ["", "-q", "-qq"]) +def test_collection_collect_only_live_logging(testdir, verbose): + testdir.makepyfile( + """ + def test_simple(): + pass + """ ) + result = testdir.runpytest("--collect-only", "--log-cli-level=INFO", verbose) + + expected_lines = [] + + if not verbose: + expected_lines.extend( + [ + "*collected 1 item*", + "**", + "*no tests ran*", + ] + ) + elif verbose == "-q": + assert "collected 1 item*" not in result.stdout.str() + expected_lines.extend( + [ + "*test_collection_collect_only_live_logging.py::test_simple*", + "no tests ran in * seconds", + ] + ) + elif verbose == "-qq": + assert "collected 1 item*" not in result.stdout.str() + expected_lines.extend(["*test_collection_collect_only_live_logging.py: 1*"]) + + result.stdout.fnmatch_lines(expected_lines) + def test_collection_logging_to_file(testdir): log_file = testdir.tmpdir.join("pytest.log").strpath From 45d36ddb47e48a7277ff1c671427be563f3139e4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 8 Jun 2019 12:44:40 -0700 Subject: [PATCH 11/78] Merge pull request #5421 from nicoddemus/raises-warning-message Link deprecation docs pytest.raises 'message' warning --- src/_pytest/deprecated.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 6283a840246..12394aca3f5 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -40,8 +40,8 @@ RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning( "The 'message' parameter is deprecated.\n" "(did you mean to use `match='some regex'` to check the exception message?)\n" - "Please comment on https://github.com/pytest-dev/pytest/issues/3974 " - "if you have concerns about removal of this parameter." + "Please see:\n" + " https://docs.pytest.org/en/4.6-maintenance/deprecations.html#message-parameter-of-pytest-raises" ) RESULT_LOG = PytestDeprecationWarning( From b8e65d03bf563d5de3a285af187fd00a2f202e5e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 11 Jun 2019 08:48:11 -0700 Subject: [PATCH 12/78] Preparing release version 4.6.3 --- CHANGELOG.rst | 16 ++++++++++++++++ changelog/5383.bugfix.rst | 2 -- changelog/5389.bugfix.rst | 1 - changelog/5390.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.3.rst | 21 +++++++++++++++++++++ 6 files changed, 38 insertions(+), 4 deletions(-) delete mode 100644 changelog/5383.bugfix.rst delete mode 100644 changelog/5389.bugfix.rst delete mode 100644 changelog/5390.bugfix.rst create mode 100644 doc/en/announce/release-4.6.3.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 715238b327b..45e31bef94b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,22 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.3 (2019-06-11) +========================= + +Bug Fixes +--------- + +- `#5383 `_: ``-q`` has again an impact on the style of the collected items + (``--collect-only``) when ``--log-cli-level`` is used. + + +- `#5389 `_: Fix regressions of `#5063 `__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. + + +- `#5390 `_: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. + + pytest 4.6.2 (2019-06-03) ========================= diff --git a/changelog/5383.bugfix.rst b/changelog/5383.bugfix.rst deleted file mode 100644 index 53e25956d56..00000000000 --- a/changelog/5383.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``-q`` has again an impact on the style of the collected items -(``--collect-only``) when ``--log-cli-level`` is used. diff --git a/changelog/5389.bugfix.rst b/changelog/5389.bugfix.rst deleted file mode 100644 index debf0a9da9d..00000000000 --- a/changelog/5389.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regressions of `#5063 `__ for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. diff --git a/changelog/5390.bugfix.rst b/changelog/5390.bugfix.rst deleted file mode 100644 index 3f57c3043d5..00000000000 --- a/changelog/5390.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 9379ae5b1f8..c8c7f243a4b 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.3 release-4.6.2 release-4.6.1 release-4.6.0 diff --git a/doc/en/announce/release-4.6.3.rst b/doc/en/announce/release-4.6.3.rst new file mode 100644 index 00000000000..0bfb355a15a --- /dev/null +++ b/doc/en/announce/release-4.6.3.rst @@ -0,0 +1,21 @@ +pytest-4.6.3 +======================================= + +pytest 4.6.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Dirk Thomas + + +Happy testing, +The pytest Development Team From 0fc11b6f3cd060b10e236269118182df80d036a9 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 13 Jun 2019 12:38:33 +0100 Subject: [PATCH 13/78] add test for stepwise attribute error Refs: #5444 --- testing/test_stepwise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 68e5989ecdb..9edd90d3d69 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -162,8 +162,8 @@ def test_stop_on_collection_errors(broken_testdir): "-v", "--strict-markers", "--stepwise", - "working_testfile.py", "broken_testfile.py", + "working_testfile.py", ) stdout = result.stdout.str() From e2fa2b621c1259e904c1ded70067d5779290422d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Jun 2019 16:45:27 -0300 Subject: [PATCH 14/78] Fix --sw crash when first file in cmdline fails to collect Fix #5444 --- changelog/5444.bugfix.rst | 1 + src/_pytest/stepwise.py | 3 ++- testing/test_stepwise.py | 26 +++++++++++++++----------- 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 changelog/5444.bugfix.rst diff --git a/changelog/5444.bugfix.rst b/changelog/5444.bugfix.rst new file mode 100644 index 00000000000..230d4b49eb6 --- /dev/null +++ b/changelog/5444.bugfix.rst @@ -0,0 +1 @@ +Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect. diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 0427cd0ea45..274f50ff0cc 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -29,6 +29,7 @@ def __init__(self, config): self.config = config self.active = config.getvalue("stepwise") self.session = None + self.report_status = "" if self.active: self.lastfailed = config.cache.get("cache/stepwise", None) @@ -104,7 +105,7 @@ def pytest_runtest_logreport(self, report): self.lastfailed = None def pytest_report_collectionfinish(self): - if self.active and self.config.getoption("verbose") >= 0: + if self.active and self.config.getoption("verbose") >= 0 and self.report_status: return "stepwise: %s" % self.report_status def pytest_sessionfinish(self, session): diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 9edd90d3d69..c099fec0f58 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -157,14 +157,18 @@ def test_change_testfile(stepwise_testdir): assert "test_success PASSED" in stdout -def test_stop_on_collection_errors(broken_testdir): - result = broken_testdir.runpytest( - "-v", - "--strict-markers", - "--stepwise", - "broken_testfile.py", - "working_testfile.py", - ) - - stdout = result.stdout.str() - assert "errors during collection" in stdout +@pytest.mark.parametrize("broken_first", [True, False]) +def test_stop_on_collection_errors(broken_testdir, broken_first): + """Stop during collection errors. We have two possible messages depending on the order (#5444), + so test both cases.""" + files = ["working_testfile.py", "broken_testfile.py"] + if broken_first: + files.reverse() + result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files) + + if broken_first: + result.stdout.fnmatch_lines( + "*Error when collecting test, stopping test execution*" + ) + else: + result.stdout.fnmatch_lines("*errors during collection*") From 43a499e6fa02436104f239f736b8ac837c200128 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Jun 2019 17:19:36 -0300 Subject: [PATCH 15/78] Remove handling of collection errors by --sw Since then pytest itself adopted the behavior of interrupting the test session on collection errors, so --sw no longer needs to handle this. The --sw behavior seems have been implemented when pytest would continue execution even if there were collection errors. --- src/_pytest/stepwise.py | 6 ------ testing/test_stepwise.py | 8 +------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 274f50ff0cc..81b9fa0f433 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -71,12 +71,6 @@ def pytest_collection_modifyitems(self, session, config, items): config.hook.pytest_deselected(items=already_passed) - def pytest_collectreport(self, report): - if self.active and report.failed: - self.session.shouldstop = ( - "Error when collecting test, stopping test execution." - ) - def pytest_runtest_logreport(self, report): # Skip this hook if plugin is not active or the test is xfailed. if not self.active or "xfail" in report.keywords: diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index c099fec0f58..3b799a830d0 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -165,10 +165,4 @@ def test_stop_on_collection_errors(broken_testdir, broken_first): if broken_first: files.reverse() result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files) - - if broken_first: - result.stdout.fnmatch_lines( - "*Error when collecting test, stopping test execution*" - ) - else: - result.stdout.fnmatch_lines("*errors during collection*") + result.stdout.fnmatch_lines("*errors during collection*") From 4e02248b84bdbb48af9c3fa5ba06b690ca6ae8e1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 13 Jun 2019 23:10:13 -0300 Subject: [PATCH 16/78] Fix test docstring --- testing/test_stepwise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 3b799a830d0..b27dd02993e 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -159,8 +159,8 @@ def test_change_testfile(stepwise_testdir): @pytest.mark.parametrize("broken_first", [True, False]) def test_stop_on_collection_errors(broken_testdir, broken_first): - """Stop during collection errors. We have two possible messages depending on the order (#5444), - so test both cases.""" + """Stop during collection errors. Broken test first or broken test last + actually surfaced a bug (#5444), so we test both situations.""" files = ["working_testfile.py", "broken_testfile.py"] if broken_first: files.reverse() From 443af11861fa14d0daf4cbe1dfae956930ce996a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 15 Jun 2019 18:54:03 +0200 Subject: [PATCH 17/78] Merge pull request #5404 from Zac-HD/helpful-mock-unwrapper Emit warning for broken object --- changelog/5404.bugfix.rst | 2 ++ src/_pytest/doctest.py | 16 +++++++++++++--- testing/test_doctest.py | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 changelog/5404.bugfix.rst diff --git a/changelog/5404.bugfix.rst b/changelog/5404.bugfix.rst new file mode 100644 index 00000000000..2187bed8b32 --- /dev/null +++ b/changelog/5404.bugfix.rst @@ -0,0 +1,2 @@ +Emit a warning when attempting to unwrap a broken object raises an exception, +for easier debugging (`#5080 `__). diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 95a80bb4fa2..659d24aeebc 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -8,6 +8,7 @@ import platform import sys import traceback +import warnings from contextlib import contextmanager import pytest @@ -17,6 +18,7 @@ from _pytest.compat import safe_getattr from _pytest.fixtures import FixtureRequest from _pytest.outcomes import Skipped +from _pytest.warning_types import PytestWarning DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" @@ -374,10 +376,18 @@ def _patch_unwrap_mock_aware(): else: def _mock_aware_unwrap(obj, stop=None): - if stop is None: - return real_unwrap(obj, stop=_is_mocked) - else: + try: + if stop is None or stop is _is_mocked: + return real_unwrap(obj, stop=_is_mocked) return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj)) + except Exception as e: + warnings.warn( + "Got %r when unwrapping %r. This is usually caused " + "by a violation of Python's object protocol; see e.g. " + "https://github.com/pytest-dev/pytest/issues/5080" % (e, obj), + PytestWarning, + ) + raise inspect.unwrap = _mock_aware_unwrap try: diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 25a35c3c136..723f1fe9642 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -3,11 +3,14 @@ from __future__ import division from __future__ import print_function +import inspect import sys import textwrap import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR +from _pytest.doctest import _is_mocked +from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule from _pytest.doctest import DoctestTextfile @@ -1237,3 +1240,25 @@ class Example(object): ) result = testdir.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) + + +class Broken: + def __getattr__(self, _): + raise KeyError("This should be an AttributeError") + + +@pytest.mark.skipif(not hasattr(inspect, "unwrap"), reason="nothing to patch") +@pytest.mark.parametrize( # pragma: no branch (lambdas are not called) + "stop", [None, _is_mocked, lambda f: None, lambda f: False, lambda f: True] +) +def test_warning_on_unwrap_of_broken_object(stop): + bad_instance = Broken() + assert inspect.unwrap.__module__ == "inspect" + with _patch_unwrap_mock_aware(): + assert inspect.unwrap.__module__ != "inspect" + with pytest.warns( + pytest.PytestWarning, match="^Got KeyError.* when unwrapping" + ): + with pytest.raises(KeyError): + inspect.unwrap(bad_instance, stop=stop) + assert inspect.unwrap.__module__ == "inspect" From d5eed3bb9c4012f2ba3b097d664d8a5d59b6edf9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 24 Jun 2019 22:05:19 -0300 Subject: [PATCH 18/78] =?UTF-8?q?Pickup=20addition=20positional=20args=20p?= =?UTF-8?q?assed=20to=20=5Fparse=5Fparametrize=5Far=E2=80=A6=20(#5483)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pickup addition positional args passed to _parse_parametrize_args --- AUTHORS | 1 + changelog/5482.bugfix.rst | 2 ++ src/_pytest/mark/structures.py | 5 +---- testing/python/metafunc.py | 13 +++++++++++++ 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 changelog/5482.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 0672d4abf6e..610d04c4b5a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -135,6 +135,7 @@ Kale Kundert Katarzyna Jachim Katerina Koukiou Kevin Cox +Kevin J. Foley Kodi B. Arfer Kostis Anagnostopoulos Kristoffer Nordström diff --git a/changelog/5482.bugfix.rst b/changelog/5482.bugfix.rst new file mode 100644 index 00000000000..c345458d18e --- /dev/null +++ b/changelog/5482.bugfix.rst @@ -0,0 +1,2 @@ +Fix bug introduced in 4.6.0 causing collection errors when passing +more than 2 positional arguments to ``pytest.mark.parametrize``. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 9602e8acfa3..bfbe71c2654 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -104,10 +104,7 @@ def extract_from(cls, parameterset, force_tuple=False): return cls(parameterset, marks=[], id=None) @staticmethod - def _parse_parametrize_args(argnames, argvalues, **_): - """It receives an ignored _ (kwargs) argument so this function can - take also calls from parametrize ignoring scope, indirect, and other - arguments...""" + def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): if not isinstance(argnames, (tuple, list)): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 945ab8627cb..d90787894ae 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1765,3 +1765,16 @@ def test_func_b(y): result.stdout.fnmatch_lines( ["*test_func_a*0*PASS*", "*test_func_a*2*PASS*", "*test_func_b*10*PASS*"] ) + + def test_parametrize_positional_args(self, testdir): + testdir.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("a", [1], False) + def test_foo(a): + pass + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) From d2f74d342ed8f51c4e57215671903b4d4dff1669 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 25 Jun 2019 12:54:34 +0100 Subject: [PATCH 19/78] fix safe_str docstring --- src/_pytest/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 7668c3a94c7..df661bfff02 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -377,7 +377,7 @@ def safe_str(v): else: def safe_str(v): - """returns v as string, converting to ascii if necessary""" + """returns v as string, converting to utf-8 if necessary""" try: return str(v) except UnicodeError: From 013d0e66c79acae06b9d61368b57f11223efadb5 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 25 Jun 2019 12:40:07 +0100 Subject: [PATCH 20/78] use safe_str to serialize Exceptions Fixes #5478 --- changelog/5478.bugfix.rst | 1 + src/_pytest/_code/code.py | 5 +++-- testing/python/raises.py | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelog/5478.bugfix.rst diff --git a/changelog/5478.bugfix.rst b/changelog/5478.bugfix.rst new file mode 100644 index 00000000000..b9e80a88ee6 --- /dev/null +++ b/changelog/5478.bugfix.rst @@ -0,0 +1 @@ +Use safe_str to serialize Exceptions in pytest.raises diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 8c73ccc6adc..e621f3ee048 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -572,8 +572,9 @@ def match(self, regexp): raised. """ __tracebackhide__ = True - if not re.search(regexp, str(self.value)): - assert 0, "Pattern '{!s}' not found in '{!s}'".format(regexp, self.value) + value = safe_str(self.value) + if not re.search(regexp, value): + assert 0, "Pattern '{!s}' not found in '{!s}'".format(regexp, value) return True diff --git a/testing/python/raises.py b/testing/python/raises.py index cd463d74b07..db34c6624f5 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -278,3 +278,7 @@ def __class__(self): with pytest.raises(CrappyClass()): pass assert "via __class__" in excinfo.value.args[0] + + def test_u(self): + with pytest.raises(AssertionError, match=u"\u2603"): + assert False, u"\u2603" From 86a4eb6008599b042cb32eae24370068bb584edb Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 27 Jun 2019 07:35:02 +0100 Subject: [PATCH 21/78] Update changelog/5478.bugfix.rst Co-Authored-By: Bruno Oliveira --- changelog/5478.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/5478.bugfix.rst b/changelog/5478.bugfix.rst index b9e80a88ee6..74d58e73489 100644 --- a/changelog/5478.bugfix.rst +++ b/changelog/5478.bugfix.rst @@ -1 +1 @@ -Use safe_str to serialize Exceptions in pytest.raises +Fix encode error when using unicode strings in exceptions with ``pytest.raises``. From f4b1c1184f840a2a7b476cf10e23c8306cdae7fb Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 27 Jun 2019 10:46:43 -0700 Subject: [PATCH 22/78] Merge pull request #5506 from asottile/fix_no_terminal Fix crash when discovery fails while using `-p no:terminal` --- changelog/5505.bugfix.rst | 1 + src/_pytest/nodes.py | 2 +- testing/test_config.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 changelog/5505.bugfix.rst diff --git a/changelog/5505.bugfix.rst b/changelog/5505.bugfix.rst new file mode 100644 index 00000000000..2d0a53b3925 --- /dev/null +++ b/changelog/5505.bugfix.rst @@ -0,0 +1 @@ +Fix crash when discovery fails while using ``-p no:terminal``. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 7342d960738..206e9ae163e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -329,7 +329,7 @@ def repr_failure(self, excinfo): # Respect explicit tbstyle option, but default to "short" # (None._repr_failure_py defaults to "long" without "fulltrace" option). - tbstyle = self.config.getoption("tbstyle") + tbstyle = self.config.getoption("tbstyle", "auto") if tbstyle == "auto": tbstyle = "short" diff --git a/testing/test_config.py b/testing/test_config.py index eefd908de59..0678a0d9015 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -15,6 +15,7 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg +from _pytest.main import EXIT_INTERRUPTED from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_OK from _pytest.main import EXIT_TESTSFAILED @@ -752,10 +753,10 @@ def pytest_addoption(parser): **{ "conftest": conftest_source, "subdir/conftest": conftest_source, - "subdir/test_foo": """ + "subdir/test_foo": """\ def test_foo(pytestconfig): assert pytestconfig.getini('foo') == 'subdir' - """, + """, } ) @@ -788,6 +789,12 @@ def pytest_internalerror(self, excrepr): assert "ValueError" in err +def test_no_terminal_discovery_error(testdir): + testdir.makepyfile("raise TypeError('oops!')") + result = testdir.runpytest("-p", "no:terminal", "--collect-only") + assert result.ret == EXIT_INTERRUPTED + + def test_load_initial_conftest_last_ordering(testdir, _config_for_test): pm = _config_for_test.pluginmanager From d3549df5b94137453b832345a9b8074f518731b0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 28 Jun 2019 18:22:19 -0700 Subject: [PATCH 23/78] Preparing release version 4.6.4 --- CHANGELOG.rst | 20 ++++++++++++++++++++ changelog/5404.bugfix.rst | 2 -- changelog/5444.bugfix.rst | 1 - changelog/5482.bugfix.rst | 2 -- changelog/5505.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.4.rst | 22 ++++++++++++++++++++++ 7 files changed, 43 insertions(+), 6 deletions(-) delete mode 100644 changelog/5404.bugfix.rst delete mode 100644 changelog/5444.bugfix.rst delete mode 100644 changelog/5482.bugfix.rst delete mode 100644 changelog/5505.bugfix.rst create mode 100644 doc/en/announce/release-4.6.4.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45e31bef94b..db0d3462be1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,26 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.4 (2019-06-28) +========================= + +Bug Fixes +--------- + +- `#5404 `_: Emit a warning when attempting to unwrap a broken object raises an exception, + for easier debugging (`#5080 `__). + + +- `#5444 `_: Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect. + + +- `#5482 `_: Fix bug introduced in 4.6.0 causing collection errors when passing + more than 2 positional arguments to ``pytest.mark.parametrize``. + + +- `#5505 `_: Fix crash when discovery fails while using ``-p no:terminal``. + + pytest 4.6.3 (2019-06-11) ========================= diff --git a/changelog/5404.bugfix.rst b/changelog/5404.bugfix.rst deleted file mode 100644 index 2187bed8b32..00000000000 --- a/changelog/5404.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Emit a warning when attempting to unwrap a broken object raises an exception, -for easier debugging (`#5080 `__). diff --git a/changelog/5444.bugfix.rst b/changelog/5444.bugfix.rst deleted file mode 100644 index 230d4b49eb6..00000000000 --- a/changelog/5444.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``--stepwise`` mode when the first file passed on the command-line fails to collect. diff --git a/changelog/5482.bugfix.rst b/changelog/5482.bugfix.rst deleted file mode 100644 index c345458d18e..00000000000 --- a/changelog/5482.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix bug introduced in 4.6.0 causing collection errors when passing -more than 2 positional arguments to ``pytest.mark.parametrize``. diff --git a/changelog/5505.bugfix.rst b/changelog/5505.bugfix.rst deleted file mode 100644 index 2d0a53b3925..00000000000 --- a/changelog/5505.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix crash when discovery fails while using ``-p no:terminal``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index c8c7f243a4b..5c6fbcad968 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.4 release-4.6.3 release-4.6.2 release-4.6.1 diff --git a/doc/en/announce/release-4.6.4.rst b/doc/en/announce/release-4.6.4.rst new file mode 100644 index 00000000000..7b35ed4f0d4 --- /dev/null +++ b/doc/en/announce/release-4.6.4.rst @@ -0,0 +1,22 @@ +pytest-4.6.4 +======================================= + +pytest 4.6.4 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Thomas Grainger +* Zac Hatfield-Dodds + + +Happy testing, +The pytest Development Team From 09dee292ca87424a763bbf0ffeb4bd0e346b76cd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 10:34:40 -0300 Subject: [PATCH 24/78] Use unicode message if regex is also unicode in ExceptionInfo.match --- src/_pytest/_code/code.py | 8 ++++++-- testing/python/raises.py | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e621f3ee048..e99575e1176 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -572,9 +572,13 @@ def match(self, regexp): raised. """ __tracebackhide__ = True - value = safe_str(self.value) + value = ( + text_type(self.value) if isinstance(regexp, text_type) else str(self.value) + ) if not re.search(regexp, value): - assert 0, "Pattern '{!s}' not found in '{!s}'".format(regexp, value) + raise AssertionError( + "Pattern '{!s}' not found in '{!s}'".format(regexp, value) + ) return True diff --git a/testing/python/raises.py b/testing/python/raises.py index db34c6624f5..ba7a04b8cfb 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -279,6 +279,8 @@ def __class__(self): pass assert "via __class__" in excinfo.value.args[0] - def test_u(self): + def test_unicode_message(self): + """pytest.raises should be able to match unicode messages when using a unicode regex (#5478) + """ with pytest.raises(AssertionError, match=u"\u2603"): assert False, u"\u2603" From a886015bfdf686021eb475ebdf2afe86f594a342 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 21:08:40 -0300 Subject: [PATCH 25/78] Test various bytes <=> unicode cases as requested in review --- src/_pytest/_code/code.py | 2 +- testing/python/raises.py | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e99575e1176..4e0c45a3785 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -577,7 +577,7 @@ def match(self, regexp): ) if not re.search(regexp, value): raise AssertionError( - "Pattern '{!s}' not found in '{!s}'".format(regexp, value) + u"Pattern '{}' not found in '{}'".format(regexp, value) ) return True diff --git a/testing/python/raises.py b/testing/python/raises.py index ba7a04b8cfb..70f4af306c2 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -4,6 +4,7 @@ import six import pytest +from _pytest.compat import dummy_context_manager from _pytest.outcomes import Failed from _pytest.warning_types import PytestDeprecationWarning @@ -279,8 +280,34 @@ def __class__(self): pass assert "via __class__" in excinfo.value.args[0] - def test_unicode_message(self): - """pytest.raises should be able to match unicode messages when using a unicode regex (#5478) - """ - with pytest.raises(AssertionError, match=u"\u2603"): - assert False, u"\u2603" + +class TestUnicodeHandling: + """Test various combinations of bytes and unicode with pytest.raises (#5478) + + https://github.com/pytest-dev/pytest/pull/5479#discussion_r298852433 + """ + + success = dummy_context_manager + py2_only = pytest.mark.skipif( + six.PY3, reason="bytes in raises only supported in Python 2" + ) + + @pytest.mark.parametrize( + "message, match, expectation", + [ + (u"\u2603", u"\u2603", success()), + (u"\u2603", u"\u2603foo", pytest.raises(AssertionError)), + pytest.param(b"hello", b"hello", success(), marks=py2_only), + pytest.param( + b"hello", b"world", pytest.raises(AssertionError), marks=py2_only + ), + pytest.param(u"hello", b"hello", success(), marks=py2_only), + pytest.param( + u"hello", b"world", pytest.raises(AssertionError), marks=py2_only + ), + ], + ) + def test_handling(self, message, match, expectation): + with expectation: + with pytest.raises(RuntimeError, match=match): + raise RuntimeError(message) From 34b4e216066cef4f301f1069981fae1cd39ceeee Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jul 2019 09:34:55 -0300 Subject: [PATCH 26/78] Include two more cases for non-ascii encoded bytes --- src/_pytest/_code/code.py | 2 +- testing/python/raises.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 4e0c45a3785..175d6fda01d 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -577,7 +577,7 @@ def match(self, regexp): ) if not re.search(regexp, value): raise AssertionError( - u"Pattern '{}' not found in '{}'".format(regexp, value) + u"Pattern {!r} not found in {!r}".format(regexp, value) ) return True diff --git a/testing/python/raises.py b/testing/python/raises.py index 70f4af306c2..9cd3ec71734 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -221,7 +221,7 @@ def test_raises_match(self): int("asdf") msg = "with base 16" - expr = r"Pattern '{}' not found in 'invalid literal for int\(\) with base 10: 'asdf''".format( + expr = r"Pattern '{}' not found in \"invalid literal for int\(\) with base 10: 'asdf'\"".format( msg ) with pytest.raises(AssertionError, match=expr): @@ -305,6 +305,18 @@ class TestUnicodeHandling: pytest.param( u"hello", b"world", pytest.raises(AssertionError), marks=py2_only ), + pytest.param( + u"😊".encode("UTF-8"), + b"world", + pytest.raises(AssertionError), + marks=py2_only, + ), + pytest.param( + u"world", + u"😊".encode("UTF-8"), + pytest.raises(AssertionError), + marks=py2_only, + ), ], ) def test_handling(self, message, match, expectation): From df0cff18ac40e5b62dcf75c2a8067f6c9a05564b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jul 2019 20:50:33 -0300 Subject: [PATCH 27/78] Handle xfail(strict=True) properly in --step-wise mode (#5555) Handle xfail(strict=True) properly in --step-wise mode --- changelog/5547.bugfix.rst | 1 + src/_pytest/stepwise.py | 2 +- testing/test_stepwise.py | 53 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 changelog/5547.bugfix.rst diff --git a/changelog/5547.bugfix.rst b/changelog/5547.bugfix.rst new file mode 100644 index 00000000000..0345bf048fc --- /dev/null +++ b/changelog/5547.bugfix.rst @@ -0,0 +1 @@ +``--step-wise`` now handles ``xfail(strict=True)`` markers properly. diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 81b9fa0f433..7225e3e4908 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -73,7 +73,7 @@ def pytest_collection_modifyitems(self, session, config, items): def pytest_runtest_logreport(self, report): # Skip this hook if plugin is not active or the test is xfailed. - if not self.active or "xfail" in report.keywords: + if not self.active: return if report.failed: diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index b27dd02993e..c05d7925dff 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -166,3 +166,56 @@ def test_stop_on_collection_errors(broken_testdir, broken_first): files.reverse() result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files) result.stdout.fnmatch_lines("*errors during collection*") + + +def test_xfail_handling(testdir): + """Ensure normal xfail is ignored, and strict xfail interrupts the session in sw mode + + (#5547) + """ + contents = """ + import pytest + def test_a(): pass + + @pytest.mark.xfail(strict={strict}) + def test_b(): assert {assert_value} + + def test_c(): pass + def test_d(): pass + """ + testdir.makepyfile(contents.format(assert_value="0", strict="False")) + result = testdir.runpytest("--sw", "-v") + result.stdout.fnmatch_lines( + [ + "*::test_a PASSED *", + "*::test_b XFAIL *", + "*::test_c PASSED *", + "*::test_d PASSED *", + "* 3 passed, 1 xfailed in *", + ] + ) + + testdir.makepyfile(contents.format(assert_value="1", strict="True")) + result = testdir.runpytest("--sw", "-v") + result.stdout.fnmatch_lines( + [ + "*::test_a PASSED *", + "*::test_b FAILED *", + "* Interrupted*", + "* 1 failed, 1 passed in *", + ] + ) + + # because we are writing to the same file, mtime might not be affected enough to + # invalidate the cache, making this next run flaky + testdir.tmpdir.join("__pycache__").remove() + testdir.makepyfile(contents.format(assert_value="0", strict="True")) + result = testdir.runpytest("--sw", "-v") + result.stdout.fnmatch_lines( + [ + "*::test_b XFAIL *", + "*::test_c PASSED *", + "*::test_d PASSED *", + "* 2 passed, 1 deselected, 1 xfailed in *", + ] + ) From acb62ba619257de8520e77bd01082f4556d2b14e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jul 2019 21:08:16 -0300 Subject: [PATCH 28/78] Fix test_stepwise::test_xfail_handling when byte code writing is disabled --- testing/test_stepwise.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index c05d7925dff..d8d11917ade 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import sys + import pytest @@ -208,7 +210,8 @@ def test_d(): pass # because we are writing to the same file, mtime might not be affected enough to # invalidate the cache, making this next run flaky - testdir.tmpdir.join("__pycache__").remove() + if not sys.dont_write_bytecode: + testdir.tmpdir.join("__pycache__").remove() testdir.makepyfile(contents.format(assert_value="0", strict="True")) result = testdir.runpytest("--sw", "-v") result.stdout.fnmatch_lines( From a92ac0d4f6f95f1b349bab77b5616ca410cd4181 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 5 Jul 2019 10:26:37 -0300 Subject: [PATCH 29/78] Fix comment in stepwise (follow up to #5555) [skip ci] (#5560) Fix comment in stepwise (follow up to #5555) [skip ci] --- src/_pytest/stepwise.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 7225e3e4908..88902595896 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -72,7 +72,6 @@ def pytest_collection_modifyitems(self, session, config, items): config.hook.pytest_deselected(items=already_passed) def pytest_runtest_logreport(self, report): - # Skip this hook if plugin is not active or the test is xfailed. if not self.active: return From 02c737fe4e823e89af4fc39a88aabda2cceb1e5b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 11 Jul 2019 18:57:03 -0300 Subject: [PATCH 30/78] Fix rmtree to remove directories with read-only files (#5588) Fix rmtree to remove directories with read-only files --- changelog/5524.bugfix.rst | 2 ++ src/_pytest/cacheprovider.py | 4 +-- src/_pytest/pathlib.py | 47 ++++++++++++++++++++++------ testing/test_tmpdir.py | 60 ++++++++++++++++++++++++++++++++++-- 4 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 changelog/5524.bugfix.rst diff --git a/changelog/5524.bugfix.rst b/changelog/5524.bugfix.rst new file mode 100644 index 00000000000..96ebbd43e09 --- /dev/null +++ b/changelog/5524.bugfix.rst @@ -0,0 +1,2 @@ +Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only, +which could lead to pytest crashing when executed a second time with the ``--basetemp`` option. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 045248cb7c2..f5c55454849 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -21,7 +21,7 @@ from .compat import _PY2 as PY2 from .pathlib import Path from .pathlib import resolve_from_str -from .pathlib import rmtree +from .pathlib import rm_rf README_CONTENT = u"""\ # pytest cache directory # @@ -51,7 +51,7 @@ class Cache(object): def for_config(cls, config): cachedir = cls.cache_dir_from_config(config) if config.getoption("cacheclear") and cachedir.exists(): - rmtree(cachedir, force=True) + rm_rf(cachedir) cachedir.mkdir() return cls(cachedir, config) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 729c41797d9..b502a5200f0 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import + import atexit import errno import fnmatch @@ -8,6 +10,7 @@ import shutil import sys import uuid +import warnings from functools import reduce from os.path import expanduser from os.path import expandvars @@ -19,6 +22,7 @@ from six.moves import map from .compat import PY36 +from _pytest.warning_types import PytestWarning if PY36: from pathlib import Path, PurePath @@ -38,17 +42,42 @@ def ensure_reset_dir(path): ensures the given path is an empty directory """ if path.exists(): - rmtree(path, force=True) + rm_rf(path) path.mkdir() -def rmtree(path, force=False): - if force: - # NOTE: ignore_errors might leave dead folders around. - # Python needs a rm -rf as a followup. - shutil.rmtree(str(path), ignore_errors=True) - else: - shutil.rmtree(str(path)) +def rm_rf(path): + """Remove the path contents recursively, even if some elements + are read-only. + """ + + def chmod_w(p): + import stat + + mode = os.stat(str(p)).st_mode + os.chmod(str(p), mode | stat.S_IWRITE) + + def force_writable_and_retry(function, p, excinfo): + p = Path(p) + + # for files, we need to recursively go upwards + # in the directories to ensure they all are also + # writable + if p.is_file(): + for parent in p.parents: + chmod_w(parent) + # stop when we reach the original path passed to rm_rf + if parent == path: + break + + chmod_w(p) + try: + # retry the function that failed + function(str(p)) + except Exception as e: + warnings.warn(PytestWarning("(rm_rf) error removing {}: {}".format(p, e))) + + shutil.rmtree(str(path), onerror=force_writable_and_retry) def find_prefixed(root, prefix): @@ -186,7 +215,7 @@ def maybe_delete_a_numbered_dir(path): garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) path.rename(garbage) - rmtree(garbage, force=True) + rm_rf(garbage) except (OSError, EnvironmentError): # known races: # * other process did a cleanup at the same time diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 40ffe98af98..0c75f74797c 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -3,6 +3,8 @@ from __future__ import division from __future__ import print_function +import os +import stat import sys import attr @@ -318,11 +320,11 @@ def test_cleanup_locked(self, tmp_path): ) def test_rmtree(self, tmp_path): - from _pytest.pathlib import rmtree + from _pytest.pathlib import rm_rf adir = tmp_path / "adir" adir.mkdir() - rmtree(adir) + rm_rf(adir) assert not adir.exists() @@ -330,9 +332,40 @@ def test_rmtree(self, tmp_path): afile = adir / "afile" afile.write_bytes(b"aa") - rmtree(adir, force=True) + rm_rf(adir) assert not adir.exists() + def test_rmtree_with_read_only_file(self, tmp_path): + """Ensure rm_rf can remove directories with read-only files in them (#5524)""" + from _pytest.pathlib import rm_rf + + fn = tmp_path / "dir/foo.txt" + fn.parent.mkdir() + + fn.touch() + + mode = os.stat(str(fn)).st_mode + os.chmod(str(fn), mode & ~stat.S_IWRITE) + + rm_rf(fn.parent) + + assert not fn.parent.is_dir() + + def test_rmtree_with_read_only_directory(self, tmp_path): + """Ensure rm_rf can remove read-only directories (#5524)""" + from _pytest.pathlib import rm_rf + + adir = tmp_path / "dir" + adir.mkdir() + + (adir / "foo.txt").touch() + mode = os.stat(str(adir)).st_mode + os.chmod(str(adir), mode & ~stat.S_IWRITE) + + rm_rf(adir) + + assert not adir.is_dir() + def test_cleanup_ignores_symlink(self, tmp_path): the_symlink = tmp_path / (self.PREFIX + "current") attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) @@ -358,3 +391,24 @@ def attempt_symlink_to(path, to_path): def test_tmpdir_equals_tmp_path(tmpdir, tmp_path): assert Path(tmpdir) == tmp_path + + +def test_basetemp_with_read_only_files(testdir): + """Integration test for #5524""" + testdir.makepyfile( + """ + import os + import stat + + def test(tmp_path): + fn = tmp_path / 'foo.txt' + fn.write_text(u'hello') + mode = os.stat(str(fn)).st_mode + os.chmod(str(fn), mode & ~stat.S_IREAD) + """ + ) + result = testdir.runpytest("--basetemp=tmp") + assert result.ret == 0 + # running a second time and ensure we don't crash + result = testdir.runpytest("--basetemp=tmp") + assert result.ret == 0 From f06ae5297b2ab59c8df4610ccdcb29797ee05100 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 20 Jul 2019 12:30:18 -0700 Subject: [PATCH 31/78] Merge pull request #5636 from asottile/fixup_sysmodules_test Fix ordering of sys modules snapshot --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 54d364ca1fd..f7cc0936cce 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -245,8 +245,8 @@ def test_inline_run_taking_and_restoring_a_sys_modules_snapshot( ): spy_factory = self.spy_factory() monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory) - original = dict(sys.modules) testdir.syspathinsert() + original = dict(sys.modules) testdir.makepyfile(import1="# you son of a silly person") testdir.makepyfile(import2="# my hovercraft is full of eels") test_mod = testdir.makepyfile( From 400393cfe4f45b3a8c6b89bbbb4041797dd3bc61 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 23 Jul 2019 11:17:42 -0300 Subject: [PATCH 32/78] Improve output when parsing an ini configuration fails (#5650) Improve output when parsing an ini configuration fails --- changelog/5650.bugfix.rst | 1 + src/_pytest/config/findpaths.py | 6 +++++- testing/test_config.py | 6 ++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelog/5650.bugfix.rst diff --git a/changelog/5650.bugfix.rst b/changelog/5650.bugfix.rst new file mode 100644 index 00000000000..db57a40b976 --- /dev/null +++ b/changelog/5650.bugfix.rst @@ -0,0 +1 @@ +Improved output when parsing an ini configuration file fails. diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 3ece3bdc130..e6779b289bc 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -33,7 +33,11 @@ def getcfg(args, config=None): for inibasename in inibasenames: p = base.join(inibasename) if exists(p): - iniconfig = py.iniconfig.IniConfig(p) + try: + iniconfig = py.iniconfig.IniConfig(p) + except py.iniconfig.ParseError as exc: + raise UsageError(str(exc)) + if ( inibasename == "setup.cfg" and "tool:pytest" in iniconfig.sections diff --git a/testing/test_config.py b/testing/test_config.py index 0678a0d9015..7cf37bf3058 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -131,6 +131,12 @@ def test_toxini_before_lower_pytestini(self, testdir): config = testdir.parseconfigure(sub) assert config.getini("minversion") == "2.0" + def test_ini_parse_error(self, testdir): + testdir.tmpdir.join("pytest.ini").write("addopts = -x") + result = testdir.runpytest() + assert result.ret != 0 + result.stderr.fnmatch_lines(["ERROR: *pytest.ini:1: no section header defined"]) + @pytest.mark.xfail(reason="probably not needed") def test_confcutdir(self, testdir): sub = testdir.mkdir("sub") From 2e345fd277ff2c1244bffeb8c51549df396d6bcd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 1 Aug 2019 09:37:02 -0300 Subject: [PATCH 33/78] Disable shallow cloning because of setuptools-scm setuptools-scm needs all tags to guess the version correctly --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 240d6cdf0f4..abcbfbac50f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,10 @@ env: global: - PYTEST_ADDOPTS=-vv +# setuptools-scm needs all tags in order to obtain a proper version +git: + depth: false + install: - python -m pip install --upgrade --pre tox From 0274c08b8ac8cbd8dc98b3df6acee3ed5242dd91 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 2 Aug 2019 10:23:12 -0300 Subject: [PATCH 34/78] Handle only known functions in rm_rf (#5627) Backport of #5627 Conflicts: - src/_pytest/pathlib.py Also had to adapt: - PermissionError into OSError with appropriate - Change keyword-only argument to **kwargs - Remove type annotations --- src/_pytest/pathlib.py | 68 ++++++++++++++++++++++++++---------------- testing/test_tmpdir.py | 66 ++++++++++++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 42 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index b502a5200f0..8d121619444 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -11,6 +11,7 @@ import sys import uuid import warnings +from functools import partial from functools import reduce from os.path import expanduser from os.path import expandvars @@ -46,38 +47,53 @@ def ensure_reset_dir(path): path.mkdir() -def rm_rf(path): - """Remove the path contents recursively, even if some elements - are read-only. - """ +def on_rm_rf_error(func, path, exc, **kwargs): + """Handles known read-only errors during rmtree.""" + start_path = kwargs["start_path"] + excvalue = exc[1] - def chmod_w(p): - import stat + if not isinstance(excvalue, OSError) or excvalue.errno not in ( + errno.EACCES, + errno.EPERM, + ): + warnings.warn( + PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue)) + ) + return - mode = os.stat(str(p)).st_mode - os.chmod(str(p), mode | stat.S_IWRITE) + if func not in (os.rmdir, os.remove, os.unlink): + warnings.warn( + PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue)) + ) + return - def force_writable_and_retry(function, p, excinfo): - p = Path(p) + # Chmod + retry. + import stat - # for files, we need to recursively go upwards - # in the directories to ensure they all are also - # writable - if p.is_file(): - for parent in p.parents: - chmod_w(parent) - # stop when we reach the original path passed to rm_rf - if parent == path: - break + def chmod_rw(p): + mode = os.stat(p).st_mode + os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR) - chmod_w(p) - try: - # retry the function that failed - function(str(p)) - except Exception as e: - warnings.warn(PytestWarning("(rm_rf) error removing {}: {}".format(p, e))) + # For files, we need to recursively go upwards in the directories to + # ensure they all are also writable. + p = Path(path) + if p.is_file(): + for parent in p.parents: + chmod_rw(str(parent)) + # stop when we reach the original path passed to rm_rf + if parent == start_path: + break + chmod_rw(str(path)) + + func(path) - shutil.rmtree(str(path), onerror=force_writable_and_retry) + +def rm_rf(path): + """Remove the path contents recursively, even if some elements + are read-only. + """ + onerror = partial(on_rm_rf_error, start_path=path) + shutil.rmtree(str(path), onerror=onerror) def find_prefixed(root, prefix): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 0c75f74797c..4ed5df21647 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -3,6 +3,7 @@ from __future__ import division from __future__ import print_function +import errno import os import stat import sys @@ -319,7 +320,20 @@ def test_cleanup_locked(self, tmp_path): p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1 ) - def test_rmtree(self, tmp_path): + def test_cleanup_ignores_symlink(self, tmp_path): + the_symlink = tmp_path / (self.PREFIX + "current") + attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) + self._do_cleanup(tmp_path) + + def test_removal_accepts_lock(self, tmp_path): + folder = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) + pathlib.create_cleanup_lock(folder) + pathlib.maybe_delete_a_numbered_dir(folder) + assert folder.is_dir() + + +class TestRmRf: + def test_rm_rf(self, tmp_path): from _pytest.pathlib import rm_rf adir = tmp_path / "adir" @@ -335,7 +349,7 @@ def test_rmtree(self, tmp_path): rm_rf(adir) assert not adir.exists() - def test_rmtree_with_read_only_file(self, tmp_path): + def test_rm_rf_with_read_only_file(self, tmp_path): """Ensure rm_rf can remove directories with read-only files in them (#5524)""" from _pytest.pathlib import rm_rf @@ -344,14 +358,17 @@ def test_rmtree_with_read_only_file(self, tmp_path): fn.touch() - mode = os.stat(str(fn)).st_mode - os.chmod(str(fn), mode & ~stat.S_IWRITE) + self.chmod_r(fn) rm_rf(fn.parent) assert not fn.parent.is_dir() - def test_rmtree_with_read_only_directory(self, tmp_path): + def chmod_r(self, path): + mode = os.stat(str(path)).st_mode + os.chmod(str(path), mode & ~stat.S_IWRITE) + + def test_rm_rf_with_read_only_directory(self, tmp_path): """Ensure rm_rf can remove read-only directories (#5524)""" from _pytest.pathlib import rm_rf @@ -359,23 +376,40 @@ def test_rmtree_with_read_only_directory(self, tmp_path): adir.mkdir() (adir / "foo.txt").touch() - mode = os.stat(str(adir)).st_mode - os.chmod(str(adir), mode & ~stat.S_IWRITE) + self.chmod_r(adir) rm_rf(adir) assert not adir.is_dir() - def test_cleanup_ignores_symlink(self, tmp_path): - the_symlink = tmp_path / (self.PREFIX + "current") - attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) - self._do_cleanup(tmp_path) + def test_on_rm_rf_error(self, tmp_path): + from _pytest.pathlib import on_rm_rf_error - def test_removal_accepts_lock(self, tmp_path): - folder = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX) - pathlib.create_cleanup_lock(folder) - pathlib.maybe_delete_a_numbered_dir(folder) - assert folder.is_dir() + adir = tmp_path / "dir" + adir.mkdir() + + fn = adir / "foo.txt" + fn.touch() + self.chmod_r(fn) + + # unknown exception + with pytest.warns(pytest.PytestWarning): + exc_info = (None, RuntimeError(), None) + on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path) + assert fn.is_file() + + permission_error = OSError() + permission_error.errno = errno.EACCES + + # unknown function + with pytest.warns(pytest.PytestWarning): + exc_info = (None, permission_error, None) + on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path) + assert fn.is_file() + + exc_info = (None, permission_error, None) + on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path) + assert not fn.is_file() def attempt_symlink_to(path, to_path): From b71f8731893c4e4b4c7d3ca97e17738519318006 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 5 Aug 2019 17:51:51 +0200 Subject: [PATCH 35/78] [4.6] Fix RuntimeError when trying to collect package with "__init__.py" only Fixes https://github.com/pytest-dev/pytest/issues/4344. --- changelog/4344.bugfix.rst | 1 + src/_pytest/main.py | 8 +++++++- testing/test_collection.py | 12 ++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 changelog/4344.bugfix.rst diff --git a/changelog/4344.bugfix.rst b/changelog/4344.bugfix.rst new file mode 100644 index 00000000000..644a6f03058 --- /dev/null +++ b/changelog/4344.bugfix.rst @@ -0,0 +1 @@ +Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index fa4d8d3d509..a9d310cb62d 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -621,7 +621,13 @@ def _collect(self, arg): # Module itself, so just use that. If this special case isn't taken, then all # the files in the package will be yielded. if argpath.basename == "__init__.py": - yield next(m[0].collect()) + try: + yield next(m[0].collect()) + except StopIteration: + # The package collects nothing with only an __init__.py + # file in it, which gets ignored by the default + # "python_files" option. + pass return for y in m: yield y diff --git a/testing/test_collection.py b/testing/test_collection.py index 1cc8938666f..52c6aa9995b 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1211,6 +1211,18 @@ def test_collect_pkg_init_and_file_in_args(testdir): ) +def test_collect_pkg_init_only(testdir): + subdir = testdir.mkdir("sub") + init = subdir.ensure("__init__.py") + init.write("def test_init(): pass") + + result = testdir.runpytest(str(init)) + result.stdout.fnmatch_lines(["*no tests ran in*"]) + + result = testdir.runpytest("-v", "-o", "python_files=*.py", str(init)) + result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) + + @pytest.mark.skipif( not hasattr(py.path.local, "mksymlinkto"), reason="symlink not available on this platform", From 7807c263bcbb98ba91b50cb2b0dcdf55c49f80e6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 5 Aug 2019 13:37:25 -0300 Subject: [PATCH 36/78] Preparing release version 4.6.5 --- CHANGELOG.rst | 22 ++++++++++++++++++++++ changelog/4344.bugfix.rst | 1 - changelog/5478.bugfix.rst | 1 - changelog/5524.bugfix.rst | 2 -- changelog/5547.bugfix.rst | 1 - changelog/5650.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.5.rst | 21 +++++++++++++++++++++ doc/en/example/parametrize.rst | 7 ++++--- doc/en/example/simple.rst | 2 +- 10 files changed, 49 insertions(+), 10 deletions(-) delete mode 100644 changelog/4344.bugfix.rst delete mode 100644 changelog/5478.bugfix.rst delete mode 100644 changelog/5524.bugfix.rst delete mode 100644 changelog/5547.bugfix.rst delete mode 100644 changelog/5650.bugfix.rst create mode 100644 doc/en/announce/release-4.6.5.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index db0d3462be1..e284dcd12ee 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,28 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.5 (2019-08-05) +========================= + +Bug Fixes +--------- + +- `#4344 `_: Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only. + + +- `#5478 `_: Fix encode error when using unicode strings in exceptions with ``pytest.raises``. + + +- `#5524 `_: Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only, + which could lead to pytest crashing when executed a second time with the ``--basetemp`` option. + + +- `#5547 `_: ``--step-wise`` now handles ``xfail(strict=True)`` markers properly. + + +- `#5650 `_: Improved output when parsing an ini configuration file fails. + + pytest 4.6.4 (2019-06-28) ========================= diff --git a/changelog/4344.bugfix.rst b/changelog/4344.bugfix.rst deleted file mode 100644 index 644a6f03058..00000000000 --- a/changelog/4344.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only. diff --git a/changelog/5478.bugfix.rst b/changelog/5478.bugfix.rst deleted file mode 100644 index 74d58e73489..00000000000 --- a/changelog/5478.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix encode error when using unicode strings in exceptions with ``pytest.raises``. diff --git a/changelog/5524.bugfix.rst b/changelog/5524.bugfix.rst deleted file mode 100644 index 96ebbd43e09..00000000000 --- a/changelog/5524.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only, -which could lead to pytest crashing when executed a second time with the ``--basetemp`` option. diff --git a/changelog/5547.bugfix.rst b/changelog/5547.bugfix.rst deleted file mode 100644 index 0345bf048fc..00000000000 --- a/changelog/5547.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -``--step-wise`` now handles ``xfail(strict=True)`` markers properly. diff --git a/changelog/5650.bugfix.rst b/changelog/5650.bugfix.rst deleted file mode 100644 index db57a40b976..00000000000 --- a/changelog/5650.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improved output when parsing an ini configuration file fails. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 5c6fbcad968..7999ca279ab 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.5 release-4.6.4 release-4.6.3 release-4.6.2 diff --git a/doc/en/announce/release-4.6.5.rst b/doc/en/announce/release-4.6.5.rst new file mode 100644 index 00000000000..6998d4e4c5f --- /dev/null +++ b/doc/en/announce/release-4.6.5.rst @@ -0,0 +1,21 @@ +pytest-4.6.5 +======================================= + +pytest 4.6.5 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Thomas Grainger + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index ef792afe7b6..2ab5e3ab10c 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -434,10 +434,11 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ...sss...sssssssss...sss... [100%] + ...ssssssssssssssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found - 12 passed, 15 skipped in 0.12 seconds + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.5' not found + 3 passed, 24 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 140f4b840f1..1dee981be77 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -440,7 +440,7 @@ Now we can profile which test functions execute the slowest: test_some_are_slow.py ... [100%] ========================= slowest 3 test durations ========================= - 0.30s call test_some_are_slow.py::test_funcslow2 + 0.31s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= From 4910036b76d846b4b7b0f80bb1291b0d1e67ea60 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 12 Aug 2019 13:20:16 -0300 Subject: [PATCH 37/78] Publish GitHub release notes after deployment (#5723) Publish GitHub release notes after deployment --- .travis.yml | 11 +++- scripts/publish_gh_release_notes.py | 86 +++++++++++++++++++++++++++++ tox.ini | 11 ++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 scripts/publish_gh_release_notes.py diff --git a/.travis.yml b/.travis.yml index abcbfbac50f..2d6883f48f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -99,8 +99,17 @@ jobs: - stage: deploy python: '3.6' - install: pip install -U setuptools setuptools_scm + install: pip install -U setuptools setuptools_scm tox script: skip + # token to upload github release notes: GH_RELEASE_NOTES_TOKEN + env: + - secure: "OjOeL7/0JUDkV00SsTs732e8vQjHynpbG9FKTNtZZJ+1Zn4Cib+hAlwmlBnvVukML0X60YpcfjnC4quDOIGLPsh5zeXnvJmYtAIIUNQXjWz8NhcGYrhyzuP1rqV22U68RTCdmOq3lMYU/W2acwHP7T49PwJtOiUM5kF120UAQ0Zi5EmkqkIvH8oM5mO9Dlver+/U7Htpz9rhKrHBXQNCMZI6yj2aUyukqB2PN2fjAlDbCF//+FmvYw9NjT4GeFOSkTCf4ER9yfqs7yglRfwiLtOCZ2qKQhWZNsSJDB89rxIRXWavJUjJKeY2EW2/NkomYJDpqJLIF4JeFRw/HhA47CYPeo6BJqyyNV+0CovL1frpWfi9UQw2cMbgFUkUIUk3F6DD59PHNIOX2R/HX56dQsw7WKl3QuHlCOkICXYg8F7Ta684IoKjeTX03/6QNOkURfDBwfGszY0FpbxrjCSWKom6RyZdyidnESaxv9RzjcIRZVh1rp8KMrwS1OrwRSdG0zjlsPr49hWMenN/8fKgcHTV4/r1Tj6mip0dorSRCrgUNIeRBKgmui6FS8642ab5JNKOxMteVPVR2sFuhjOQ0Jy+PmvceYY9ZMWc3+/B/KVh0dZ3hwvLGZep/vxDS2PwCA5/xw31714vT5LxidKo8yECjBynMU/wUTTS695D3NY=" + addons: + apt: + packages: + # required by publish_gh_release_notes + - pandoc + after_deploy: tox -e publish_gh_release_notes deploy: provider: pypi user: nicoddemus diff --git a/scripts/publish_gh_release_notes.py b/scripts/publish_gh_release_notes.py new file mode 100644 index 00000000000..f6c956fadef --- /dev/null +++ b/scripts/publish_gh_release_notes.py @@ -0,0 +1,86 @@ +""" +Script used to publish GitHub release notes extracted from CHANGELOG.rst. + +This script is meant to be executed after a successful deployment in Travis. + +Uses the following environment variables: + +* GIT_TAG: the name of the tag of the current commit. +* GH_RELEASE_NOTES_TOKEN: a personal access token with 'repo' permissions. It should be encrypted using: + + $travis encrypt GH_RELEASE_NOTES_TOKEN= -r pytest-dev/pytest + + And the contents pasted in the ``deploy.env.secure`` section in the ``travis.yml`` file. + +The script also requires ``pandoc`` to be previously installed in the system. + +Requires Python3.6+. +""" +import os +import re +import sys +from pathlib import Path + +import github3 +import pypandoc + + +def publish_github_release(token, tag_name, body): + github = github3.login(token=token) + repo = github.repository("pytest-dev", "pytest") + return repo.create_release(tag_name=tag_name, body=body) + + +def parse_changelog(tag_name): + p = Path(__file__).parent.parent / "CHANGELOG.rst" + changelog_lines = p.read_text(encoding="UTF-8").splitlines() + + title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)") + consuming_version = False + version_lines = [] + for line in changelog_lines: + m = title_regex.match(line) + if m: + # found the version we want: start to consume lines until we find the next version title + if m.group(1) == tag_name: + consuming_version = True + # found a new version title while parsing the version we want: break out + elif consuming_version: + break + if consuming_version: + version_lines.append(line) + + return "\n".join(version_lines) + + +def convert_rst_to_md(text): + return pypandoc.convert_text(text, "md", format="rst") + + +def main(argv): + if len(argv) > 1: + tag_name = argv[1] + else: + tag_name = os.environ.get("TRAVIS_TAG") + if not tag_name: + print("tag_name not given and $TRAVIS_TAG not set", file=sys.stderr) + return 1 + + token = os.environ.get("GH_RELEASE_NOTES_TOKEN") + if not token: + print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr) + return 1 + + rst_body = parse_changelog(tag_name) + md_body = convert_rst_to_md(rst_body) + if not publish_github_release(token, tag_name, md_body): + print("Could not publish release notes:", file=sys.stderr) + print(md_body, file=sys.stderr) + return 5 + + print(f"Release notes for {tag_name} published successfully") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tox.ini b/tox.ini index 0b1be0d33fe..f2cb74e681d 100644 --- a/tox.ini +++ b/tox.ini @@ -136,6 +136,17 @@ deps = wheel commands = python scripts/release.py {posargs} +[testenv:publish_gh_release_notes] +description = create GitHub release after deployment +basepython = python3.6 +usedevelop = True +passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG +deps = + github3.py + pypandoc +commands = python scripts/publish_gh_release_notes.py + + [pytest] minversion = 2.0 addopts = -ra -p pytester --strict-markers From 2fbea0e5e4175472d4936fa81245cc8c52086792 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 14 Aug 2019 21:46:57 -0300 Subject: [PATCH 38/78] Merge pull request #5740 from nicoddemus/use-repo-env-var Use TRAVIS_REPO_SLUG instead of hard-coding pytest-dev/pytest --- scripts/publish_gh_release_notes.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/publish_gh_release_notes.py b/scripts/publish_gh_release_notes.py index f6c956fadef..23f7b40adaa 100644 --- a/scripts/publish_gh_release_notes.py +++ b/scripts/publish_gh_release_notes.py @@ -25,9 +25,10 @@ import pypandoc -def publish_github_release(token, tag_name, body): +def publish_github_release(slug, token, tag_name, body): github = github3.login(token=token) - repo = github.repository("pytest-dev", "pytest") + owner, repo = slug.split("/") + repo = github.repository(owner, repo) return repo.create_release(tag_name=tag_name, body=body) @@ -71,14 +72,22 @@ def main(argv): print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr) return 1 + slug = os.environ.get("TRAVIS_REPO_SLUG") + if not slug: + print("TRAVIS_REPO_SLUG not set", file=sys.stderr) + return 1 + rst_body = parse_changelog(tag_name) md_body = convert_rst_to_md(rst_body) - if not publish_github_release(token, tag_name, md_body): + if not publish_github_release(slug, token, tag_name, md_body): print("Could not publish release notes:", file=sys.stderr) print(md_body, file=sys.stderr) return 5 - print(f"Release notes for {tag_name} published successfully") + print() + print(f"Release notes for {tag_name} published successfully:") + print(f"https://github.com/{slug}/releases/tag/{tag_name}") + print() return 0 From 7a96f3f9701b9a3009dc7ddff8f2029bfcfc72aa Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 16 Aug 2019 15:57:01 -0300 Subject: [PATCH 39/78] Merge pull request #5750 from nicoddemus/fix-gh-publish-notes Forward $TRAVIS_REPO_SLUG for GH publish notes --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f2cb74e681d..ab9f581f74b 100644 --- a/tox.ini +++ b/tox.ini @@ -140,7 +140,7 @@ commands = python scripts/release.py {posargs} description = create GitHub release after deployment basepython = python3.6 usedevelop = True -passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG +passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG TRAVIS_REPO_SLUG deps = github3.py pypandoc From 7718d8c97267174ffcaca2e5205de4c8732b58b5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 19 Aug 2019 16:58:47 -0300 Subject: [PATCH 40/78] Fix linting --- scripts/publish_gh_release_notes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/publish_gh_release_notes.py b/scripts/publish_gh_release_notes.py index 23f7b40adaa..3ff946b58a3 100644 --- a/scripts/publish_gh_release_notes.py +++ b/scripts/publish_gh_release_notes.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Script used to publish GitHub release notes extracted from CHANGELOG.rst. From 9191857b5ff6a945f0ffb7cdc3dfbe719fd3cfa2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 20 Aug 2019 19:11:14 -0300 Subject: [PATCH 41/78] Do not update pip on Azure Avoid upgrading pip because it is giving this error on py34: Requirement already up-to-date: pip in c:\hostedtoolcache\windows\python\3.4.4\x64\lib\site-packages (19.2.1) ERROR: Package 'pip' requires a different Python: 3.4.4 not in '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' [error]Cmd.exe exited with code '1'. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8e50486de55..d33a9e09b84 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -91,7 +91,7 @@ jobs: condition: eq(variables['python.needs_vc'], True) displayName: 'Install VC for py27' - - script: python -m pip install --upgrade pip && python -m pip install tox + - script: python -m pip install tox displayName: 'Install tox' - script: | From aa79b1c00c63a0cbba756adec2b2e1e51da82a45 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 29 Aug 2019 09:48:58 -0700 Subject: [PATCH 42/78] [4.6] fixes for python4 --- changelog/5801.trivial.rst | 1 + src/_pytest/pastebin.py | 2 +- src/_pytest/pytester.py | 2 +- testing/acceptance_test.py | 2 +- testing/python/raises.py | 2 +- testing/test_monkeypatch.py | 4 ++-- testing/test_pastebin.py | 4 ++-- testing/test_warnings.py | 2 +- 8 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 changelog/5801.trivial.rst diff --git a/changelog/5801.trivial.rst b/changelog/5801.trivial.rst new file mode 100644 index 00000000000..80b1f823844 --- /dev/null +++ b/changelog/5801.trivial.rst @@ -0,0 +1 @@ +Fixes python version checks (detected by ``flake8-2020``) in case python4 becomes a thing. diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 3f4171207a3..41576a615b0 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -79,7 +79,7 @@ def create_new_paste(contents): params = { "code": contents, - "lexer": "python3" if sys.version_info[0] == 3 else "python", + "lexer": "python3" if sys.version_info[0] >= 3 else "python", "expiry": "1week", } url = "https://bpaste.net" diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 605451630a6..f1d739c9917 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1124,7 +1124,7 @@ def handle_timeout(): if timeout is None: ret = popen.wait() - elif six.PY3: + elif not six.PY2: try: ret = popen.wait(timeout) except subprocess.TimeoutExpired: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 7016cf13b6e..4079cddb614 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -223,7 +223,7 @@ def foo(): "conftest.py:2: in foo", " import qwerty", "E {}: No module named {q}qwerty{q}".format( - exc_name, q="'" if six.PY3 else "" + exc_name, q="" if six.PY2 else "'" ), ] ) diff --git a/testing/python/raises.py b/testing/python/raises.py index 9cd3ec71734..fa25d9f73ee 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -289,7 +289,7 @@ class TestUnicodeHandling: success = dummy_context_manager py2_only = pytest.mark.skipif( - six.PY3, reason="bytes in raises only supported in Python 2" + not six.PY2, reason="bytes in raises only supported in Python 2" ) @pytest.mark.parametrize( diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 9b2f4550240..961c57e06a3 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -209,7 +209,7 @@ class TestEnvironWarnings(object): VAR_NAME = u"PYTEST_INTERNAL_MY_VAR" - @pytest.mark.skipif(six.PY3, reason="Python 2 only test") + @pytest.mark.skipif(not six.PY2, reason="Python 2 only test") def test_setenv_unicode_key(self, monkeypatch): with pytest.warns( pytest.PytestWarning, @@ -217,7 +217,7 @@ def test_setenv_unicode_key(self, monkeypatch): ): monkeypatch.setenv(self.VAR_NAME, "2") - @pytest.mark.skipif(six.PY3, reason="Python 2 only test") + @pytest.mark.skipif(not six.PY2, reason="Python 2 only test") def test_delenv_unicode_key(self, monkeypatch): with pytest.warns( pytest.PytestWarning, diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 9491f6d9019..bac8f980240 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -74,7 +74,7 @@ def test(): """ ) result = testdir.runpytest("--pastebin=all") - if sys.version_info[0] == 3: + if sys.version_info[0] >= 3: expected_msg = "*assert '☺' == 1*" else: expected_msg = "*assert '\\xe2\\x98\\xba' == 1*" @@ -126,7 +126,7 @@ def test_create_new_paste(self, pastebin, mocked_urlopen): assert len(mocked_urlopen) == 1 url, data = mocked_urlopen[0] assert type(data) is bytes - lexer = "python3" if sys.version_info[0] == 3 else "python" + lexer = "python3" if sys.version_info[0] >= 3 else "python" assert url == "https://bpaste.net" assert "lexer=%s" % lexer in data.decode() assert "code=full-paste-contents" in data.decode() diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 092604d7db0..65f57e024f3 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -569,7 +569,7 @@ def test_hidden_by_system(self, testdir, monkeypatch): assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() -@pytest.mark.skipif(six.PY3, reason="Python 2 only issue") +@pytest.mark.skipif(not six.PY2, reason="Python 2 only issue") def test_infinite_loop_warning_against_unicode_usage_py2(testdir): """ We need to be careful when raising the warning about unicode usage with "warnings.warn" From 914441557cf38465383aa4891c9a9a984a2df9db Mon Sep 17 00:00:00 2001 From: Michael Goerz Date: Fri, 30 Aug 2019 15:34:03 -0400 Subject: [PATCH 43/78] Fix "lexer" being used when uploading to bpaste.net Closes #5806. --- changelog/5806.bugfix.rst | 1 + src/_pytest/pastebin.py | 6 +----- testing/test_pastebin.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 changelog/5806.bugfix.rst diff --git a/changelog/5806.bugfix.rst b/changelog/5806.bugfix.rst new file mode 100644 index 00000000000..ec887768ddd --- /dev/null +++ b/changelog/5806.bugfix.rst @@ -0,0 +1 @@ +Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text". diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 41576a615b0..7a3e80231c2 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -77,11 +77,7 @@ def create_new_paste(contents): from urllib.request import urlopen from urllib.parse import urlencode - params = { - "code": contents, - "lexer": "python3" if sys.version_info[0] >= 3 else "python", - "expiry": "1week", - } + params = {"code": contents, "lexer": "text", "expiry": "1week"} url = "https://bpaste.net" response = urlopen(url, data=urlencode(params).encode("ascii")).read() m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8")) diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index bac8f980240..02efdb55407 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -126,7 +126,7 @@ def test_create_new_paste(self, pastebin, mocked_urlopen): assert len(mocked_urlopen) == 1 url, data = mocked_urlopen[0] assert type(data) is bytes - lexer = "python3" if sys.version_info[0] >= 3 else "python" + lexer = "text" assert url == "https://bpaste.net" assert "lexer=%s" % lexer in data.decode() assert "code=full-paste-contents" in data.decode() From c03e46f1ad99e8b01a71c6ad43acfc6240670051 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 6 Oct 2019 08:47:17 -0300 Subject: [PATCH 44/78] Fix warnings with attrs 19.2 and fix object assertions (#5902) Fix warnings with attrs 19.2 and fix object assertions --- src/_pytest/assertion/util.py | 5 ++++- src/_pytest/compat.py | 11 +++++++++-- src/_pytest/mark/structures.py | 4 +++- testing/test_assertion.py | 3 ++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 1fee64ce08d..c382f1c6091 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -12,6 +12,7 @@ from ..compat import Sequence from _pytest import outcomes from _pytest._io.saferepr import saferepr +from _pytest.compat import ATTRS_EQ_FIELD # The _reprcompare attribute on the util module is used by the new assertion # interpretation code and assertion rewriter to detect this plugin was @@ -374,7 +375,9 @@ def _compare_eq_cls(left, right, verbose, type_fns): fields_to_check = [field for field, info in all_fields.items() if info.compare] elif isattrs(left): all_fields = left.__attrs_attrs__ - fields_to_check = [field.name for field in all_fields if field.cmp] + fields_to_check = [ + field.name for field in all_fields if getattr(field, ATTRS_EQ_FIELD) + ] same = [] diff = [] diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index df661bfff02..38531a9d48c 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -13,6 +13,7 @@ import sys from contextlib import contextmanager +import attr import py import six from six import text_type @@ -406,8 +407,8 @@ def _setup_collect_fakemodule(): pytest.collect = ModuleType("pytest.collect") pytest.collect.__all__ = [] # used for setns - for attr in COLLECT_FAKEMODULE_ATTRIBUTES: - setattr(pytest.collect, attr, getattr(pytest, attr)) + for attribute in COLLECT_FAKEMODULE_ATTRIBUTES: + setattr(pytest.collect, attribute, getattr(pytest, attribute)) if _PY2: @@ -455,3 +456,9 @@ def dec(fn): else: from functools import lru_cache # noqa: F401 + + +if getattr(attr, "__version_info__", ()) >= (19, 2): + ATTRS_EQ_FIELD = "eq" +else: + ATTRS_EQ_FIELD = "cmp" diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index bfbe71c2654..0ccd8141dd9 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -8,6 +8,7 @@ import six from ..compat import ascii_escaped +from ..compat import ATTRS_EQ_FIELD from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET @@ -377,7 +378,8 @@ def __repr__(self): return "" % (self.node,) -@attr.s(cmp=False, hash=False) +# mypy cannot find this overload, remove when on attrs>=19.2 +@attr.s(hash=False, **{ATTRS_EQ_FIELD: False}) # type: ignore class NodeMarkers(object): """ internal structure for storing marks belonging to a node diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 2085ffd8b44..a94dcd701fa 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -14,6 +14,7 @@ from _pytest import outcomes from _pytest.assertion import truncate from _pytest.assertion import util +from _pytest.compat import ATTRS_EQ_FIELD PY3 = sys.version_info >= (3, 0) @@ -687,7 +688,7 @@ def test_attrs_with_attribute_comparison_off(self): @attr.s class SimpleDataObject(object): field_a = attr.ib() - field_b = attr.ib(cmp=False) + field_b = attr.ib(**{ATTRS_EQ_FIELD: False}) left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "b") From b9a8465ce465039f1913becb6455f3bc34213a12 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jul 2019 21:46:43 -0300 Subject: [PATCH 45/78] Replace importlib_metadata with importlib.metadata on Python 3.8+ (#5539) --- changelog/5523.bugfix.rst | 1 + changelog/5537.bugfix.rst | 2 ++ setup.py | 2 +- src/_pytest/compat.py | 6 ++++++ src/_pytest/config/__init__.py | 2 +- testing/acceptance_test.py | 2 +- testing/test_assertion.py | 3 ++- testing/test_config.py | 3 +-- testing/test_entry_points.py | 2 +- 9 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 changelog/5523.bugfix.rst create mode 100644 changelog/5537.bugfix.rst diff --git a/changelog/5523.bugfix.rst b/changelog/5523.bugfix.rst new file mode 100644 index 00000000000..5155b92b156 --- /dev/null +++ b/changelog/5523.bugfix.rst @@ -0,0 +1 @@ +Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+. diff --git a/changelog/5537.bugfix.rst b/changelog/5537.bugfix.rst new file mode 100644 index 00000000000..0263e8cdf4c --- /dev/null +++ b/changelog/5537.bugfix.rst @@ -0,0 +1,2 @@ +Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the +standard library on Python 3.8+. diff --git a/setup.py b/setup.py index 18d32201caf..491a6f55460 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', "pluggy>=0.12,<1.0", - "importlib-metadata>=0.12", + 'importlib-metadata>=0.12;python_version<"3.8"', "wcwidth", ] diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 38531a9d48c..d0add530268 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -62,6 +62,12 @@ def spec_from_file_location(*_, **__): return None +if sys.version_info >= (3, 8): + from importlib import metadata as importlib_metadata # noqa +else: + import importlib_metadata # noqa + + def _format_args(func): return str(signature(func)) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6d44bf3d773..b35faffb9df 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -13,7 +13,6 @@ import types import warnings -import importlib_metadata import py import six from packaging.version import Version @@ -31,6 +30,7 @@ from _pytest import deprecated from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback +from _pytest.compat import importlib_metadata from _pytest.compat import lru_cache from _pytest.compat import safe_str from _pytest.outcomes import fail diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 4079cddb614..9b33930a06c 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -9,11 +9,11 @@ import types import attr -import importlib_metadata import py import six import pytest +from _pytest.compat import importlib_metadata from _pytest.main import EXIT_NOTESTSCOLLECTED from _pytest.main import EXIT_USAGEERROR from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG diff --git a/testing/test_assertion.py b/testing/test_assertion.py index a94dcd701fa..225362e64ef 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -180,7 +180,8 @@ def check(values, value): return check """, "mainwrapper.py": """\ - import pytest, importlib_metadata + import pytest + from _pytest.compat import importlib_metadata class DummyEntryPoint(object): name = 'spam' diff --git a/testing/test_config.py b/testing/test_config.py index 7cf37bf3058..217879038f4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -6,10 +6,9 @@ import sys import textwrap -import importlib_metadata - import _pytest._code import pytest +from _pytest.compat import importlib_metadata from _pytest.config import _iter_rewritable_modules from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index ad64d004f1c..95ebc415b15 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -3,7 +3,7 @@ from __future__ import division from __future__ import print_function -import importlib_metadata +from _pytest.compat import importlib_metadata def test_pytest_entry_points_are_identical(): From d526053af33313fd2c7689a2c03da8237c13f27a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 6 Oct 2019 07:58:23 -0400 Subject: [PATCH 46/78] Add changelog entry for #5902 --- changelog/5902.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5902.bugfix.rst diff --git a/changelog/5902.bugfix.rst b/changelog/5902.bugfix.rst new file mode 100644 index 00000000000..dccb4975776 --- /dev/null +++ b/changelog/5902.bugfix.rst @@ -0,0 +1 @@ +Fix warnings about deprecated ``cmp`` attribute in ``attrs>=19.2``. \ No newline at end of file From 62c0d82d6473d2263e57e76612976f1d7cc82e11 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 28 Sep 2019 10:20:54 -0400 Subject: [PATCH 47/78] Use 'python3' instead of 'python3.6' on tox This allows us to use python3.7+ to use tox --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index ab9f581f74b..b788020f1c1 100644 --- a/tox.ini +++ b/tox.ini @@ -93,7 +93,7 @@ commands = [testenv:regen] changedir = doc/en skipsdist = True -basepython = python3.6 +basepython = python3 deps = sphinx PyYAML @@ -125,7 +125,7 @@ commands = [testenv:release] decription = do a release, required posarg of the version number -basepython = python3.6 +basepython = python3 usedevelop = True passenv = * deps = @@ -138,7 +138,7 @@ commands = python scripts/release.py {posargs} [testenv:publish_gh_release_notes] description = create GitHub release after deployment -basepython = python3.6 +basepython = python3 usedevelop = True passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG TRAVIS_REPO_SLUG deps = From 5b499bafb24ee0347b1dc25f3f670602818e18a7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 11 Oct 2019 17:02:08 -0400 Subject: [PATCH 48/78] Preparing release version 4.6.6 --- CHANGELOG.rst | 26 ++++++++++++++++++++++++++ changelog/5523.bugfix.rst | 1 - changelog/5537.bugfix.rst | 2 -- changelog/5801.trivial.rst | 1 - changelog/5806.bugfix.rst | 1 - changelog/5902.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.6.rst | 20 ++++++++++++++++++++ doc/en/example/reportingdemo.rst | 4 ++-- doc/en/example/simple.rst | 2 +- doc/en/getting-started.rst | 2 +- 11 files changed, 51 insertions(+), 10 deletions(-) delete mode 100644 changelog/5523.bugfix.rst delete mode 100644 changelog/5537.bugfix.rst delete mode 100644 changelog/5801.trivial.rst delete mode 100644 changelog/5806.bugfix.rst delete mode 100644 changelog/5902.bugfix.rst create mode 100644 doc/en/announce/release-4.6.6.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e284dcd12ee..d04bd888f5c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,32 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.6 (2019-10-11) +========================= + +Bug Fixes +--------- + +- `#5523 `_: Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+. + + +- `#5537 `_: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the + standard library on Python 3.8+. + + +- `#5806 `_: Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text". + + +- `#5902 `_: Fix warnings about deprecated ``cmp`` attribute in ``attrs>=19.2``. + + + +Trivial/Internal Changes +------------------------ + +- `#5801 `_: Fixes python version checks (detected by ``flake8-2020``) in case python4 becomes a thing. + + pytest 4.6.5 (2019-08-05) ========================= diff --git a/changelog/5523.bugfix.rst b/changelog/5523.bugfix.rst deleted file mode 100644 index 5155b92b156..00000000000 --- a/changelog/5523.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+. diff --git a/changelog/5537.bugfix.rst b/changelog/5537.bugfix.rst deleted file mode 100644 index 0263e8cdf4c..00000000000 --- a/changelog/5537.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the -standard library on Python 3.8+. diff --git a/changelog/5801.trivial.rst b/changelog/5801.trivial.rst deleted file mode 100644 index 80b1f823844..00000000000 --- a/changelog/5801.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fixes python version checks (detected by ``flake8-2020``) in case python4 becomes a thing. diff --git a/changelog/5806.bugfix.rst b/changelog/5806.bugfix.rst deleted file mode 100644 index ec887768ddd..00000000000 --- a/changelog/5806.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix "lexer" being used when uploading to bpaste.net from ``--pastebin`` to "text". diff --git a/changelog/5902.bugfix.rst b/changelog/5902.bugfix.rst deleted file mode 100644 index dccb4975776..00000000000 --- a/changelog/5902.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix warnings about deprecated ``cmp`` attribute in ``attrs>=19.2``. \ No newline at end of file diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 7999ca279ab..af3c4ac8094 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.6 release-4.6.5 release-4.6.4 release-4.6.3 diff --git a/doc/en/announce/release-4.6.6.rst b/doc/en/announce/release-4.6.6.rst new file mode 100644 index 00000000000..c47a31695b2 --- /dev/null +++ b/doc/en/announce/release-4.6.6.rst @@ -0,0 +1,20 @@ +pytest-4.6.6 +======================================= + +pytest 4.6.6 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Michael Goerz + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 928c365cafc..d00c9836265 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -436,7 +436,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: items = [1, 2, 3] print("items is %r" % items) > a, b = items.pop() - E TypeError: 'int' object is not iterable + E TypeError: cannot unpack non-iterable int object failure_demo.py:182: TypeError --------------------------- Captured stdout call --------------------------- @@ -515,7 +515,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_z2_type_error(self): items = 3 > a, b = items - E TypeError: 'int' object is not iterable + E TypeError: cannot unpack non-iterable int object failure_demo.py:222: TypeError ______________________ TestMoreErrors.test_startswith ______________________ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 1dee981be77..140f4b840f1 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -440,7 +440,7 @@ Now we can profile which test functions execute the slowest: test_some_are_slow.py ... [100%] ========================= slowest 3 test durations ========================= - 0.31s call test_some_are_slow.py::test_funcslow2 + 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index d6c62cbe8c0..882385b2960 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py + This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest.py .. _`simpletest`: From 3edcc71c41cec04fa4b174465619f05b50201789 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 15 Aug 2019 23:54:03 +0200 Subject: [PATCH 49/78] ci: add codecov.yml to turn comments off The only benefit for me is to get notified about finished builds, but that might happen to early anyway. Apart from that they are rather big and distract from actual comments. (cherry picked from commit d50198a3ff1458b6b175cb9ae274a5a98be965ab) --- codecov.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..a0a308588e2 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,7 @@ +coverage: + status: + project: true + patch: true + changes: true + +comment: off From 0084fd9783ed6b699903f6f636adafa09bade25f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 23 Oct 2019 19:22:26 -0300 Subject: [PATCH 50/78] Review rm_rf handling of FileNotFoundErrors (#6044) Review rm_rf handling of FileNotFoundErrors Conflicts: src/_pytest/pathlib.py testing/test_tmpdir.py --- changelog/6044.bugfix.rst | 3 +++ src/_pytest/pathlib.py | 27 +++++++++++++++++++++------ testing/test_tmpdir.py | 7 ++++++- 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 changelog/6044.bugfix.rst diff --git a/changelog/6044.bugfix.rst b/changelog/6044.bugfix.rst new file mode 100644 index 00000000000..575bd979649 --- /dev/null +++ b/changelog/6044.bugfix.rst @@ -0,0 +1,3 @@ +Properly ignore ``FileNotFoundError`` (``OSError.errno == NOENT`` in Python 2) exceptions when trying to remove old temporary directories, +for instance when multiple processes try to remove the same directory (common with ``pytest-xdist`` +for example). diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 8d121619444..42071f43104 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -48,24 +48,38 @@ def ensure_reset_dir(path): def on_rm_rf_error(func, path, exc, **kwargs): - """Handles known read-only errors during rmtree.""" + """Handles known read-only errors during rmtree. + + The returned value is used only by our own tests. + """ start_path = kwargs["start_path"] - excvalue = exc[1] + exctype, excvalue = exc[:2] + + # another process removed the file in the middle of the "rm_rf" (xdist for example) + # more context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 + if isinstance(excvalue, OSError) and excvalue.errno == errno.ENOENT: + return False if not isinstance(excvalue, OSError) or excvalue.errno not in ( errno.EACCES, errno.EPERM, ): warnings.warn( - PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue)) + PytestWarning( + "(rm_rf) error removing {}\n{}: {}".format(path, exctype, excvalue) + ) ) - return + return False if func not in (os.rmdir, os.remove, os.unlink): warnings.warn( - PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue)) + PytestWarning( + "(rm_rf) unknown function {} when removing {}:\n{}: {}".format( + path, func, exctype, excvalue + ) + ) ) - return + return False # Chmod + retry. import stat @@ -86,6 +100,7 @@ def chmod_rw(p): chmod_rw(str(path)) func(path) + return True def rm_rf(path): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 4ed5df21647..d5291f5a9e7 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -398,9 +398,14 @@ def test_on_rm_rf_error(self, tmp_path): on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path) assert fn.is_file() + # we ignore FileNotFoundError + file_not_found = OSError() + file_not_found.errno = errno.ENOENT + exc_info = (None, file_not_found, None) + assert not on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path) + permission_error = OSError() permission_error.errno = errno.EACCES - # unknown function with pytest.warns(pytest.PytestWarning): exc_info = (None, permission_error, None) From a8c16d9b75ac604b9038d175a2d0ca89fec932a2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:42:17 +0100 Subject: [PATCH 51/78] pre-commit: upgrade black This brings https://github.com/psf/black/pull/826, which helps with https://github.com/psf/black/issues/601. --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34895e1a30f..3aee45c62c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: doc/en/example/py2py3/test_py2.py repos: -- repo: https://github.com/python/black - rev: 19.3b0 +- repo: https://github.com/psf/black + rev: 19.10b0 hooks: - id: black args: [--safe, --quiet] From 3d8649b206ad67acc4c762dd23a6bcf332862806 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:51:02 +0100 Subject: [PATCH 52/78] Remove (now) unnecessary fmt: off --- src/_pytest/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index b35faffb9df..a643936958c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -116,13 +116,13 @@ def directory_arg(path, optname): # Plugins that cannot be disabled via "-p no:X" currently. -essential_plugins = ( # fmt: off +essential_plugins = ( "mark", "main", "runner", "fixtures", "helpconfig", # Provides -p. -) # fmt: on +) default_plugins = essential_plugins + ( "python", From 097acaf11bd54fd827d48b82a5ab21460ba84e80 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:53:29 +0100 Subject: [PATCH 53/78] re-run black --- src/_pytest/python_api.py | 2 +- src/_pytest/recwarn.py | 2 +- testing/deprecated_test.py | 4 ++-- testing/python/fixtures.py | 6 +++--- testing/test_collection.py | 8 ++++---- testing/test_mark.py | 4 ++-- testing/test_recwarn.py | 2 +- testing/test_skipping.py | 2 +- testing/test_tmpdir.py | 2 +- testing/test_unittest.py | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index df09aa32d8f..f6e475c3a24 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -694,7 +694,7 @@ def raises(expected_exception, *args, **kwargs): return RaisesContext(expected_exception, message, match_expr) elif isinstance(args[0], str): warnings.warn(deprecated.RAISES_EXEC, stacklevel=2) - code, = args + (code,) = args assert isinstance(code, str) frame = sys._getframe(1) loc = frame.f_locals.copy() diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 574c6a1cced..7abf2e93550 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -95,7 +95,7 @@ def warns(expected_warning, *args, **kwargs): return WarningsChecker(expected_warning, match_expr=match_expr) elif isinstance(args[0], str): warnings.warn(WARNS_EXEC, stacklevel=2) - code, = args + (code,) = args assert isinstance(code, str) frame = sys._getframe(1) loc = frame.f_locals.copy() diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index a072f6b210c..70460214fbd 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -152,7 +152,7 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( - testdir + testdir, ): from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST @@ -181,7 +181,7 @@ def test_func(): def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( - testdir + testdir, ): from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index bd1b1d97541..4869431c639 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -464,7 +464,7 @@ def test_func(something): pass assert repr(req).find(req.function.__name__) != -1 def test_request_attributes_method(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestB(object): @@ -492,7 +492,7 @@ def test_method(self, something): pass """ ) - item1, = testdir.genitems([modcol]) + (item1,) = testdir.genitems([modcol]) assert item1.name == "test_method" arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs assert len(arg2fixturedefs) == 1 @@ -756,7 +756,7 @@ def test_second(): def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") - item, = testdir.genitems([modcol]) + (item,) = testdir.genitems([modcol]) req = fixtures.FixtureRequest(item) assert req.fspath == modcol.fspath diff --git a/testing/test_collection.py b/testing/test_collection.py index 52c6aa9995b..0fbbbec54df 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -494,7 +494,7 @@ def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) items, hookrec = testdir.inline_genitems(id) - item, = items + (item,) = items assert item.name == "test_func" newid = item.nodeid assert newid == id @@ -613,9 +613,9 @@ def test_serialization_byid(self, testdir): testdir.makepyfile("def test_func(): pass") items, hookrec = testdir.inline_genitems() assert len(items) == 1 - item, = items + (item,) = items items2, hookrec = testdir.inline_genitems(item.nodeid) - item2, = items2 + (item2,) = items2 assert item2.name == item.name assert item2.fspath == item.fspath @@ -630,7 +630,7 @@ def test_method(self): arg = p.basename + "::TestClass::test_method" items, hookrec = testdir.inline_genitems(arg) assert len(items) == 1 - item, = items + (item,) = items assert item.nodeid.endswith("TestClass::test_method") # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"] diff --git a/testing/test_mark.py b/testing/test_mark.py index dd9d352309d..727bd9420c2 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1008,7 +1008,7 @@ def test_custom_mark_parametrized(obj_type): def test_pytest_param_id_requires_string(): with pytest.raises(TypeError) as excinfo: pytest.param(id=True) - msg, = excinfo.value.args + (msg,) = excinfo.value.args if six.PY2: assert msg == "Expected id to be a string, got : True" else: @@ -1025,7 +1025,7 @@ def test_pytest_param_warning_on_unknown_kwargs(): # typo, should be marks= pytest.param(1, 2, mark=pytest.mark.xfail()) assert warninfo[0].filename == __file__ - msg, = warninfo[0].message.args + (msg,) = warninfo[0].message.args assert msg == ( "pytest.param() got unexpected keyword arguments: ['mark'].\n" "This will be an error in future versions." diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 2246085f59e..d652219df3e 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -225,7 +225,7 @@ def test_strings(self): assert len(warninfo) == 3 for w in warninfo: assert w.filename == __file__ - msg, = w.message.args + (msg,) = w.message.args assert msg.startswith("warns(..., 'code(as_a_string)') is deprecated") def test_function(self): diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 55119ae1269..7834971125a 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -136,7 +136,7 @@ def test_func(): ) def test_skipif_class(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestClass(object): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index d5291f5a9e7..7622342b1d7 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -273,7 +273,7 @@ def test_lock_register_cleanup_removal(self, tmp_path): registry = [] register_cleanup_lock_removal(lock, register=registry.append) - cleanup_func, = registry + (cleanup_func,) = registry assert lock.is_file() diff --git a/testing/test_unittest.py b/testing/test_unittest.py index e9982b3d5b9..6b721d1c0ce 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -388,7 +388,7 @@ def test_hello(self): def test_testcase_totally_incompatible_exception_info(testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ from unittest import TestCase class MyTestCase(TestCase): From 192f6992d2c82f129c1951a212970a678c176e79 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 12 Jul 2019 14:35:34 -0300 Subject: [PATCH 54/78] Include root tag in generated XML (#5550) Include root tag in generated XML --- changelog/5477.bugfix.rst | 1 + src/_pytest/junitxml.py | 21 ++++++++++----------- testing/test_junitxml.py | 30 +++++++++++++++++++++++++++--- 3 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 changelog/5477.bugfix.rst diff --git a/changelog/5477.bugfix.rst b/changelog/5477.bugfix.rst new file mode 100644 index 00000000000..c9c9386e996 --- /dev/null +++ b/changelog/5477.bugfix.rst @@ -0,0 +1 @@ +The XML file produced by ``--junitxml`` now correctly contain a ```` root element. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 3972113cbb0..6f21a0638d8 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -667,18 +667,17 @@ def pytest_sessionfinish(self): ) logfile.write('') - logfile.write( - Junit.testsuite( - self._get_global_properties_node(), - [x.to_xml() for x in self.node_reporters_ordered], - name=self.suite_name, - errors=self.stats["error"], - failures=self.stats["failure"], - skipped=self.stats["skipped"], - tests=numtests, - time="%.3f" % suite_time_delta, - ).unicode(indent=0) + suite_node = Junit.testsuite( + self._get_global_properties_node(), + [x.to_xml() for x in self.node_reporters_ordered], + name=self.suite_name, + errors=self.stats["error"], + failures=self.stats["failure"], + skipped=self.stats["skipped"], + tests=numtests, + time="%.3f" % suite_time_delta, ) + logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) logfile.close() def pytest_terminal_summary(self, terminalreporter): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 4c21c94d365..a09f16fb720 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -47,6 +47,16 @@ def find_first_by_tag(self, tag): def _by_tag(self, tag): return self.__node.getElementsByTagName(tag) + @property + def children(self): + return [type(self)(x) for x in self.__node.childNodes] + + @property + def get_unique_child(self): + children = self.children + assert len(children) == 1 + return children[0] + def find_nth_by_tag(self, tag, n): items = self._by_tag(tag) try: @@ -81,7 +91,7 @@ def tag(self): return self.__node.tagName @property - def next_siebling(self): + def next_sibling(self): return type(self)(self.__node.nextSibling) @@ -390,11 +400,11 @@ def test_fail(): fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="ValueError: 42") assert "ValueError" in fnode.toxml() - systemout = fnode.next_siebling + systemout = fnode.next_sibling assert systemout.tag == "system-out" assert "hello-stdout" in systemout.toxml() assert "info msg" not in systemout.toxml() - systemerr = systemout.next_siebling + systemerr = systemout.next_sibling assert systemerr.tag == "system-err" assert "hello-stderr" in systemerr.toxml() assert "info msg" not in systemerr.toxml() @@ -1101,6 +1111,20 @@ def test_x(i): assert failed == ["test_x[22]"] +def test_root_testsuites_tag(testdir): + testdir.makepyfile( + """ + def test_x(): + pass + """ + ) + _, dom = runandparse(testdir) + root = dom.get_unique_child + assert root.tag == "testsuites" + suite_node = root.get_unique_child + assert suite_node.tag == "testsuite" + + def test_runs_twice(testdir): f = testdir.makepyfile( """ From 6f43eee106937b49bfa8e1f28a5a1aac091aa837 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 5 Dec 2019 15:29:25 -0500 Subject: [PATCH 55/78] Preparing release version 4.6.7 --- CHANGELOG.rst | 14 ++++++++++++++ changelog/5477.bugfix.rst | 1 - changelog/6044.bugfix.rst | 3 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.7.rst | 19 +++++++++++++++++++ doc/en/example/simple.rst | 2 +- 6 files changed, 35 insertions(+), 5 deletions(-) delete mode 100644 changelog/5477.bugfix.rst delete mode 100644 changelog/6044.bugfix.rst create mode 100644 doc/en/announce/release-4.6.7.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d04bd888f5c..caacbc28374 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,20 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.7 (2019-12-05) +========================= + +Bug Fixes +--------- + +- `#5477 `_: The XML file produced by ``--junitxml`` now correctly contain a ```` root element. + + +- `#6044 `_: Properly ignore ``FileNotFoundError`` (``OSError.errno == NOENT`` in Python 2) exceptions when trying to remove old temporary directories, + for instance when multiple processes try to remove the same directory (common with ``pytest-xdist`` + for example). + + pytest 4.6.6 (2019-10-11) ========================= diff --git a/changelog/5477.bugfix.rst b/changelog/5477.bugfix.rst deleted file mode 100644 index c9c9386e996..00000000000 --- a/changelog/5477.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -The XML file produced by ``--junitxml`` now correctly contain a ```` root element. diff --git a/changelog/6044.bugfix.rst b/changelog/6044.bugfix.rst deleted file mode 100644 index 575bd979649..00000000000 --- a/changelog/6044.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Properly ignore ``FileNotFoundError`` (``OSError.errno == NOENT`` in Python 2) exceptions when trying to remove old temporary directories, -for instance when multiple processes try to remove the same directory (common with ``pytest-xdist`` -for example). diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index af3c4ac8094..b1b3f11345e 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.7 release-4.6.6 release-4.6.5 release-4.6.4 diff --git a/doc/en/announce/release-4.6.7.rst b/doc/en/announce/release-4.6.7.rst new file mode 100644 index 00000000000..0e6cf6a950a --- /dev/null +++ b/doc/en/announce/release-4.6.7.rst @@ -0,0 +1,19 @@ +pytest-4.6.7 +======================================= + +pytest 4.6.7 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Bruno Oliveira +* Daniel Hahler + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 140f4b840f1..81f2dd855aa 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -442,7 +442,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 - 0.10s call test_some_are_slow.py::test_funcfast + 0.11s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= incremental testing - test steps From 8b9482e39c932b1d32de6e4007f54e9980555306 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 5 Aug 2019 10:19:15 -0300 Subject: [PATCH 56/78] Add hostname and timestamp to JUnit XML testsuite tag (#5692) Add hostname and timestamp to JUnit XML testsuite tag Conflicts: testing/test_junitxml.py --- changelog/5471.feature.rst | 1 + src/_pytest/junitxml.py | 4 ++++ testing/test_junitxml.py | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 changelog/5471.feature.rst diff --git a/changelog/5471.feature.rst b/changelog/5471.feature.rst new file mode 100644 index 00000000000..154b64ea763 --- /dev/null +++ b/changelog/5471.feature.rst @@ -0,0 +1 @@ +JUnit XML now includes a timestamp and hostname in the testsuite tag. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 6f21a0638d8..7858d8f3165 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -15,9 +15,11 @@ import functools import os +import platform import re import sys import time +from datetime import datetime import py import six @@ -676,6 +678,8 @@ def pytest_sessionfinish(self): skipped=self.stats["skipped"], tests=numtests, time="%.3f" % suite_time_delta, + timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), + hostname=platform.node(), ) logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) logfile.close() diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index a09f16fb720..7d6dfde9bc8 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -4,7 +4,9 @@ from __future__ import print_function import os +import platform import sys +from datetime import datetime from xml.dom import minidom import py @@ -145,6 +147,30 @@ def test_xpass(): node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5) + def test_hostname_in_xml(self, testdir): + testdir.makepyfile( + """ + def test_pass(): + pass + """ + ) + result, dom = runandparse(testdir) + node = dom.find_first_by_tag("testsuite") + node.assert_attr(hostname=platform.node()) + + def test_timestamp_in_xml(self, testdir): + testdir.makepyfile( + """ + def test_pass(): + pass + """ + ) + start_time = datetime.now() + result, dom = runandparse(testdir) + node = dom.find_first_by_tag("testsuite") + timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") + assert start_time <= timestamp < datetime.now() + def test_timing_function(self, testdir): testdir.makepyfile( """ From f9ebe3c607116b758a35d26903c088d25c1ef693 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 12 Dec 2019 09:35:25 -0300 Subject: [PATCH 57/78] Bugfix 5430 pass logs to junit report (#6274) Bugfix 5430 pass logs to junit report --- AUTHORS | 1 + changelog/5430.bugfix.rst | 1 + src/_pytest/junitxml.py | 2 ++ testing/test_junitxml.py | 42 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 changelog/5430.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 610d04c4b5a..a19e38bbf13 100644 --- a/AUTHORS +++ b/AUTHORS @@ -58,6 +58,7 @@ Christian Theunert Christian Tismer Christopher Gilling Christopher Dignam +Claudio Madotto CrazyMerlyn Cyrus Maden Damian Skrzypczak diff --git a/changelog/5430.bugfix.rst b/changelog/5430.bugfix.rst new file mode 100644 index 00000000000..734685063c8 --- /dev/null +++ b/changelog/5430.bugfix.rst @@ -0,0 +1 @@ +junitxml: Logs for failed test are now passed to junit report in case the test fails during call phase. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 7858d8f3165..853dcb77449 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -597,6 +597,8 @@ def pytest_runtest_logreport(self, report): if report.when == "call": reporter.append_failure(report) self.open_reports.append(report) + if not self.log_passing_tests: + reporter.write_captured_output(report) else: reporter.append_error(report) elif report.skipped: diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 7d6dfde9bc8..a08959c50f3 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1409,3 +1409,45 @@ def test_func(): 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 + + +@parametrize_families +@pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"]) +def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430( + testdir, junit_logging, run_and_parse, xunit_family +): + testdir.makeini( + """ + [pytest] + junit_log_passing_tests=False + junit_family={family} + """.format( + family=xunit_family + ) + ) + testdir.makepyfile( + """ + import pytest + import logging + import sys + + def test_func(): + logging.warning('hello') + assert 0 + """ + ) + result, dom = run_and_parse( + "-o", "junit_logging=%s" % junit_logging, family=xunit_family + ) + assert result.ret == 1 + node = dom.find_first_by_tag("testcase") + if junit_logging == "system-out": + assert len(node.find_by_tag("system-err")) == 0 + assert len(node.find_by_tag("system-out")) == 1 + elif junit_logging == "system-err": + assert len(node.find_by_tag("system-err")) == 1 + assert len(node.find_by_tag("system-out")) == 0 + else: + assert junit_logging == "no" + assert len(node.find_by_tag("system-err")) == 0 + assert len(node.find_by_tag("system-out")) == 0 From 8bfe434f75296530a44c8bff7087a532703b4ca8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 12 Dec 2019 16:44:07 -0300 Subject: [PATCH 58/78] Drop validation against multiple xmlfamilies This was not backported to 4.6 before, so had to adapt the test slightly to the old method of validation. --- testing/test_junitxml.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index a08959c50f3..ba529cf331d 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1411,19 +1411,15 @@ def test_func(): assert len(node.find_by_tag("system-out")) == 0 -@parametrize_families @pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"]) def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430( - testdir, junit_logging, run_and_parse, xunit_family + testdir, junit_logging ): testdir.makeini( """ [pytest] junit_log_passing_tests=False - junit_family={family} - """.format( - family=xunit_family - ) + """ ) testdir.makepyfile( """ @@ -1436,9 +1432,7 @@ def test_func(): assert 0 """ ) - result, dom = run_and_parse( - "-o", "junit_logging=%s" % junit_logging, family=xunit_family - ) + result, dom = runandparse(testdir, "-o", "junit_logging=%s" % junit_logging) assert result.ret == 1 node = dom.find_first_by_tag("testcase") if junit_logging == "system-out": From 1d021540a36f508786a88d6efcdb7e8d55566bf9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 12 Dec 2019 16:47:47 -0300 Subject: [PATCH 59/78] Drop 3.4 testing on Azure Azure no longer supports testing on Python 3.4 --- azure-pipelines.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d33a9e09b84..5184610fd18 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -48,12 +48,6 @@ jobs: # pypy3: # python.version: 'pypy3' # tox.env: 'pypy3' - py34-xdist: - python.version: '3.4' - tox.env: 'py34-xdist' - # Coverage for: - # - _pytest.compat._bytes_to_ascii - PYTEST_COVERAGE: '1' py35-xdist: python.version: '3.5' tox.env: 'py35-xdist' From b08c599bad85fb91f965fd24ac30f65b2da1f52b Mon Sep 17 00:00:00 2001 From: Ryan Mast Date: Thu, 12 Dec 2019 21:09:21 -0800 Subject: [PATCH 60/78] Ensure colorama versions is no newer than 0.4.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 491a6f55460..2e81874e5a2 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ "atomicwrites>=1.0", 'funcsigs>=1.0;python_version<"3.0"', 'pathlib2>=2.2.0;python_version<"3.6"', - 'colorama;sys_platform=="win32"', + 'colorama<=0.4.1;sys_platform=="win32"', "pluggy>=0.12,<1.0", 'importlib-metadata>=0.12;python_version<"3.8"', "wcwidth", From fb8395d93f9d50205c4a1da52481ae474a5fc670 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Dec 2019 07:45:18 -0300 Subject: [PATCH 61/78] Create CHANGELOG for #6340 --- changelog/6340.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/6340.trivial.rst diff --git a/changelog/6340.trivial.rst b/changelog/6340.trivial.rst new file mode 100644 index 00000000000..6debef98696 --- /dev/null +++ b/changelog/6340.trivial.rst @@ -0,0 +1 @@ +Pin ``colorama`` to ``0.4.1`` as later versions no longer support Python 3.4. From 53b08730e4dbd3446c3f079b7861ef3f57ffcbec Mon Sep 17 00:00:00 2001 From: Ryan Mast Date: Fri, 13 Dec 2019 10:07:10 -0800 Subject: [PATCH 62/78] Pin the colorama version only for Python 3.4 --- changelog/6340.trivial.rst | 1 - changelog/6345.trivial.rst | 1 + setup.py | 3 ++- 3 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 changelog/6340.trivial.rst create mode 100644 changelog/6345.trivial.rst diff --git a/changelog/6340.trivial.rst b/changelog/6340.trivial.rst deleted file mode 100644 index 6debef98696..00000000000 --- a/changelog/6340.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Pin ``colorama`` to ``0.4.1`` as later versions no longer support Python 3.4. diff --git a/changelog/6345.trivial.rst b/changelog/6345.trivial.rst new file mode 100644 index 00000000000..e954583143a --- /dev/null +++ b/changelog/6345.trivial.rst @@ -0,0 +1 @@ +Pin ``colorama`` to ``0.4.1`` only for Python 3.4 so newer Python versions can still receive colorama updates. diff --git a/setup.py b/setup.py index 2e81874e5a2..1053bce4a5a 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,8 @@ "atomicwrites>=1.0", 'funcsigs>=1.0;python_version<"3.0"', 'pathlib2>=2.2.0;python_version<"3.6"', - 'colorama<=0.4.1;sys_platform=="win32"', + 'colorama<=0.4.1;sys_platform=="win32" and python_version=="3.4"', + 'colorama;sys_platform=="win32" and python_version!="3.4"', "pluggy>=0.12,<1.0", 'importlib-metadata>=0.12;python_version<"3.8"', "wcwidth", From 2d398d870604b4d2f95b4a0157b97553008e762a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 19 Dec 2019 14:42:09 -0800 Subject: [PATCH 63/78] Preparing release version 4.6.8 --- CHANGELOG.rst | 23 +++++++++++++++++++++++ changelog/5430.bugfix.rst | 1 - changelog/5471.feature.rst | 1 - changelog/6345.trivial.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.8.rst | 20 ++++++++++++++++++++ doc/en/example/parametrize.rst | 7 +++---- doc/en/example/reportingdemo.rst | 4 ++-- doc/en/example/simple.rst | 2 +- doc/en/getting-started.rst | 2 +- 10 files changed, 51 insertions(+), 11 deletions(-) delete mode 100644 changelog/5430.bugfix.rst delete mode 100644 changelog/5471.feature.rst delete mode 100644 changelog/6345.trivial.rst create mode 100644 doc/en/announce/release-4.6.8.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index caacbc28374..8fea3e9913b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,29 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.8 (2019-12-19) +========================= + +Features +-------- + +- `#5471 `_: JUnit XML now includes a timestamp and hostname in the testsuite tag. + + + +Bug Fixes +--------- + +- `#5430 `_: junitxml: Logs for failed test are now passed to junit report in case the test fails during call phase. + + + +Trivial/Internal Changes +------------------------ + +- `#6345 `_: Pin ``colorama`` to ``0.4.1`` only for Python 3.4 so newer Python versions can still receive colorama updates. + + pytest 4.6.7 (2019-12-05) ========================= diff --git a/changelog/5430.bugfix.rst b/changelog/5430.bugfix.rst deleted file mode 100644 index 734685063c8..00000000000 --- a/changelog/5430.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -junitxml: Logs for failed test are now passed to junit report in case the test fails during call phase. diff --git a/changelog/5471.feature.rst b/changelog/5471.feature.rst deleted file mode 100644 index 154b64ea763..00000000000 --- a/changelog/5471.feature.rst +++ /dev/null @@ -1 +0,0 @@ -JUnit XML now includes a timestamp and hostname in the testsuite tag. diff --git a/changelog/6345.trivial.rst b/changelog/6345.trivial.rst deleted file mode 100644 index e954583143a..00000000000 --- a/changelog/6345.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Pin ``colorama`` to ``0.4.1`` only for Python 3.4 so newer Python versions can still receive colorama updates. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index b1b3f11345e..2ea08be7856 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.8 release-4.6.7 release-4.6.6 release-4.6.5 diff --git a/doc/en/announce/release-4.6.8.rst b/doc/en/announce/release-4.6.8.rst new file mode 100644 index 00000000000..3c04e5dbe9b --- /dev/null +++ b/doc/en/announce/release-4.6.8.rst @@ -0,0 +1,20 @@ +pytest-4.6.8 +======================================= + +pytest 4.6.8 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Ryan Mast + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 2ab5e3ab10c..e3fd4564dec 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -434,11 +434,10 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ...ssssssssssssssssssssssss [100%] + ......sss......ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found - SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.5' not found - 3 passed, 24 skipped in 0.12 seconds + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.5' not found + 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index d00c9836265..928c365cafc 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -436,7 +436,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: items = [1, 2, 3] print("items is %r" % items) > a, b = items.pop() - E TypeError: cannot unpack non-iterable int object + E TypeError: 'int' object is not iterable failure_demo.py:182: TypeError --------------------------- Captured stdout call --------------------------- @@ -515,7 +515,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_z2_type_error(self): items = 3 > a, b = items - E TypeError: cannot unpack non-iterable int object + E TypeError: 'int' object is not iterable failure_demo.py:222: TypeError ______________________ TestMoreErrors.test_startswith ______________________ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 81f2dd855aa..140f4b840f1 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -442,7 +442,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 - 0.11s call test_some_are_slow.py::test_funcfast + 0.10s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= incremental testing - test steps diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 882385b2960..d6c62cbe8c0 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest.py + This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py .. _`simpletest`: From e49282f72cbab0a8e93c42b72dafb85789448bb9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 3 Dec 2019 19:49:20 -0300 Subject: [PATCH 64/78] Fix assertion rewriting module detection for egg dists Fix #6301 --- changelog/6301.bugfix.rst | 1 + src/_pytest/config/__init__.py | 53 +++++++++++++++++++++++++++++++++- testing/test_config.py | 16 ++++++---- 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 changelog/6301.bugfix.rst diff --git a/changelog/6301.bugfix.rst b/changelog/6301.bugfix.rst new file mode 100644 index 00000000000..f13c83343f6 --- /dev/null +++ b/changelog/6301.bugfix.rst @@ -0,0 +1 @@ +Fix assertion rewriting for egg-based distributions and ``editable`` installs (``pip install --editable``). diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index a643936958c..372c7c68fea 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -622,16 +622,67 @@ def __repr__(self): def _iter_rewritable_modules(package_files): + """ + Given an iterable of file names in a source distribution, return the "names" that should + be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should + be added as "pytest_mock" in the assertion rewrite mechanism. + + This function has to deal with dist-info based distributions and egg based distributions + (which are still very much in use for "editable" installs). + + Here are the file names as seen in a dist-info based distribution: + + pytest_mock/__init__.py + pytest_mock/_version.py + pytest_mock/plugin.py + pytest_mock.egg-info/PKG-INFO + + Here are the file names as seen in an egg based distribution: + + src/pytest_mock/__init__.py + src/pytest_mock/_version.py + src/pytest_mock/plugin.py + src/pytest_mock.egg-info/PKG-INFO + LICENSE + setup.py + + We have to take in account those two distribution flavors in order to determine which + names should be considered for assertion rewriting. + + More information: + https://github.com/pytest-dev/pytest-mock/issues/167 + """ + package_files = list(package_files) + seen_some = False for fn in package_files: is_simple_module = "/" not in fn and fn.endswith(".py") is_package = fn.count("/") == 1 and fn.endswith("__init__.py") if is_simple_module: module_name, _ = os.path.splitext(fn) - yield module_name + # we ignore "setup.py" at the root of the distribution + if module_name != "setup": + seen_some = True + yield module_name elif is_package: package_name = os.path.dirname(fn) + seen_some = True yield package_name + if not seen_some: + # at this point we did not find any packages or modules suitable for assertion + # rewriting, so we try again by stripping the first path component (to account for + # "src" based source trees for example) + # this approach lets us have the common case continue to be fast, as egg-distributions + # are rarer + new_package_files = [] + for fn in package_files: + parts = fn.split("/") + new_fn = "/".join(parts[1:]) + if new_fn: + new_package_files.append(new_fn) + if new_package_files: + yield from _iter_rewritable_modules(new_package_files) + class Config(object): """ access to configuration values, pluginmanager and plugin hooks. """ diff --git a/testing/test_config.py b/testing/test_config.py index 217879038f4..d0e5937303f 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -431,15 +431,21 @@ def test_confcutdir_check_isdir(self, testdir): @pytest.mark.parametrize( "names, expected", [ + # dist-info based distributions root are files as will be put in PYTHONPATH (["bar.py"], ["bar"]), - (["foo", "bar.py"], []), - (["foo", "bar.pyc"], []), - (["foo", "__init__.py"], ["foo"]), - (["foo", "bar", "__init__.py"], []), + (["foo/bar.py"], ["bar"]), + (["foo/bar.pyc"], []), + (["foo/__init__.py"], ["foo"]), + (["bar/__init__.py", "xz.py"], ["bar", "xz"]), + (["setup.py"], []), + # egg based distributions root contain the files from the dist root + (["src/bar/__init__.py"], ["bar"]), + (["src/bar/__init__.py", "setup.py"], ["bar"]), + (["source/python/bar/__init__.py", "setup.py"], ["bar"]), ], ) def test_iter_rewritable_modules(self, names, expected): - assert list(_iter_rewritable_modules(["/".join(names)])) == expected + assert list(_iter_rewritable_modules(names)) == expected class TestConfigFromdictargs(object): From d622f12f69910996db917cc8b216ac6f1d88bafd Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Wed, 25 Dec 2019 19:44:39 +0800 Subject: [PATCH 65/78] Fix compatibility for Python 2 --- src/_pytest/config/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 372c7c68fea..63d12104060 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -681,7 +681,8 @@ def _iter_rewritable_modules(package_files): if new_fn: new_package_files.append(new_fn) if new_package_files: - yield from _iter_rewritable_modules(new_package_files) + for _module in _iter_rewritable_modules(new_package_files): + yield _module class Config(object): From 035f51ab71fec625ba7d6853f3deebf3f77b4219 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 3 Jan 2020 01:01:32 +0200 Subject: [PATCH 66/78] Update copyright year to 2020 --- LICENSE | 2 +- README.rst | 2 +- doc/en/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 477af2d2e45..d14fb7ff4b3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2004-2019 Holger Krekel and others +Copyright (c) 2004-2020 Holger Krekel and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.rst b/README.rst index 44fa8ac728d..69408978703 100644 --- a/README.rst +++ b/README.rst @@ -131,7 +131,7 @@ Tidelift will coordinate the fix and disclosure. License ------- -Copyright Holger Krekel and others, 2004-2019. +Copyright Holger Krekel and others, 2004-2020. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. diff --git a/doc/en/conf.py b/doc/en/conf.py index 5daa15a069e..59c8d833aeb 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -65,7 +65,7 @@ # General information about the project. project = u"pytest" year = datetime.datetime.utcnow().year -copyright = u"2015–2019 , holger krekel and pytest-dev team" +copyright = u"2015–2020, holger krekel and pytest-dev team" # The language for content autogenerated by Sphinx. Refer to documentation From 897f1a3ef4b9ad9266fa784b6f3f1a0bb3bc5fe9 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 4 Jan 2020 09:53:49 +0200 Subject: [PATCH 67/78] Update copyright year --- doc/en/conf.py | 2 +- doc/en/index.rst | 2 +- doc/en/license.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/conf.py b/doc/en/conf.py index 59c8d833aeb..39ba5e4f854 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -275,7 +275,7 @@ epub_title = u"pytest" epub_author = u"holger krekel at merlinux eu" epub_publisher = u"holger krekel at merlinux eu" -epub_copyright = u"2013, holger krekel et alii" +epub_copyright = u"2013-2020, holger krekel et alii" # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/doc/en/index.rst b/doc/en/index.rst index 3ace95effe6..edcbbeb84e7 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -87,7 +87,7 @@ Consult the :ref:`Changelog ` page for fixes and enhancements of each License ------- -Copyright Holger Krekel and others, 2004-2017. +Copyright Holger Krekel and others, 2004-2020. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. diff --git a/doc/en/license.rst b/doc/en/license.rst index 5ee55cf96f8..c6c10bbf358 100644 --- a/doc/en/license.rst +++ b/doc/en/license.rst @@ -9,7 +9,7 @@ Distributed under the terms of the `MIT`_ license, pytest is free and open sourc The MIT License (MIT) - Copyright (c) 2004-2017 Holger Krekel and others + Copyright (c) 2004-2020 Holger Krekel and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From f6a5578d5cc780887d98a18beb5a922ccd9c27d3 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 4 Jan 2020 04:23:39 -0800 Subject: [PATCH 68/78] Preparing release version 4.6.9 --- CHANGELOG.rst | 9 +++++++++ changelog/6301.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.9.rst | 21 +++++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) delete mode 100644 changelog/6301.bugfix.rst create mode 100644 doc/en/announce/release-4.6.9.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8fea3e9913b..4305c742904 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,15 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.9 (2020-01-04) +========================= + +Bug Fixes +--------- + +- `#6301 `_: Fix assertion rewriting for egg-based distributions and ``editable`` installs (``pip install --editable``). + + pytest 4.6.8 (2019-12-19) ========================= diff --git a/changelog/6301.bugfix.rst b/changelog/6301.bugfix.rst deleted file mode 100644 index f13c83343f6..00000000000 --- a/changelog/6301.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix assertion rewriting for egg-based distributions and ``editable`` installs (``pip install --editable``). diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 2ea08be7856..4cfd9fa0ffa 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.9 release-4.6.8 release-4.6.7 release-4.6.6 diff --git a/doc/en/announce/release-4.6.9.rst b/doc/en/announce/release-4.6.9.rst new file mode 100644 index 00000000000..ae0478c52d9 --- /dev/null +++ b/doc/en/announce/release-4.6.9.rst @@ -0,0 +1,21 @@ +pytest-4.6.9 +======================================= + +pytest 4.6.9 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Felix Yan +* Hugo + + +Happy testing, +The pytest Development Team From 24898e06404fdade3c37c9229402ea817d068e75 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 5 Jan 2020 14:15:51 -0300 Subject: [PATCH 69/78] Remove usage of parser module, deprecated in Python 3.9 Fix #6404 --- changelog/6404.trivial.rst | 1 + src/_pytest/_code/source.py | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) create mode 100644 changelog/6404.trivial.rst diff --git a/changelog/6404.trivial.rst b/changelog/6404.trivial.rst new file mode 100644 index 00000000000..8252098b6a2 --- /dev/null +++ b/changelog/6404.trivial.rst @@ -0,0 +1 @@ +Remove usage of ``parser`` module, deprecated in Python 3.9. diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index c8a4b6adf38..b35e97b9cec 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -123,18 +123,13 @@ def isparseable(self, deindent=True): """ return True if source is parseable, heuristically deindenting it by default. """ - from parser import suite as syntax_checker - if deindent: source = str(self.deindent()) else: source = str(self) try: - # compile(source+'\n', "x", "exec") - syntax_checker(source + "\n") - except KeyboardInterrupt: - raise - except Exception: + ast.parse(source) + except (SyntaxError, ValueError, TypeError): return False else: return True From c5831ac98f8adb6c5c3e8438df05803a660a9814 Mon Sep 17 00:00:00 2001 From: Fernando Mez Date: Mon, 9 Mar 2020 15:13:57 -0300 Subject: [PATCH 70/78] Fix CI config and coverage report --- .github/workflows/main.yml | 191 ++++++++++++++++++++++++++++++++ .travis.yml | 152 ++++++------------------- scripts/append_codecov_token.py | 35 ++++++ scripts/report-coverage.sh | 18 +++ tox.ini | 1 + 5 files changed, 277 insertions(+), 120 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 scripts/append_codecov_token.py create mode 100755 scripts/report-coverage.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000000..a006fb092c8 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,191 @@ +name: main + +on: + push: + branches: + - 4.6.x + tags: + - "*" + + pull_request: + branches: + - 4.6.x + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + name: [ + "windows-py27", + "windows-py35", + "windows-py36", + "windows-py37", + "windows-py37-pluggy", + "windows-py38", + + "ubuntu-py27-pluggy", + "ubuntu-py27-nobyte", + "ubuntu-py37", + "ubuntu-py37-pluggy", + "ubuntu-py37-pexpect-py37-twisted", + "ubuntu-py37-freeze", + "ubuntu-pypy", + "ubuntu-pypy3", + + "macos-py27", + "macos-py38", + + ] + + include: + # Windows jobs + - name: "windows-py27" + python: "2.7" + os: windows-latest + tox_env: "py27-xdist" + use_coverage: true + - name: "windows-py35" + python: "3.5" + os: windows-latest + tox_env: "py35-xdist" + use_coverage: true + - name: "windows-py36" + python: "3.6" + os: windows-latest + tox_env: "py36-xdist" + use_coverage: true + - name: "windows-py37" + python: "3.7" + os: windows-latest + tox_env: "py37-twisted-numpy" + use_coverage: true + - name: "windows-py37-pluggy" + python: "3.7" + os: windows-latest + tox_env: "py37-pluggymaster-xdist" + use_coverage: true + - name: "windows-py38" + python: "3.8" + os: windows-latest + tox_env: "py38-xdist" + use_coverage: true + + # Ubuntu jobs – find the rest of them in .travis.yml + - name: "ubuntu-py27-pluggy" + python: "2.7" + os: ubuntu-latest + tox_env: "py27-pluggymaster-xdist" + use_coverage: true + - name: "ubuntu-py27-nobyte" + python: "2.7" + os: ubuntu-latest + tox_env: "py27-nobyte-numpy-xdist" + use_coverage: true + - name: "ubuntu-py37" + python: "3.7" + os: ubuntu-latest + tox_env: "py37-lsof-numpy-xdist" + use_coverage: true + - name: "ubuntu-py37-pluggy" + python: "3.7" + os: ubuntu-latest + tox_env: "py37-pluggymaster-xdist" + use_coverage: true + - name: "ubuntu-py37-pexpect-py37-twisted" + python: "3.7" + os: ubuntu-latest + tox_env: "py37-pexpect,py37-twisted" + use_coverage: true + - name: "ubuntu-py37-freeze" + python: "3.7" + os: ubuntu-latest + tox_env: "py37-freeze" + - name: "ubuntu-pypy" + python: "pypy2" + os: ubuntu-latest + tox_env: "pypy-xdist" + use_coverage: true + - name: "ubuntu-pypy3" + python: "pypy3" + os: ubuntu-latest + tox_env: "pypy3-xdist" + use_coverage: true + + # MacOS jobs + - name: "macos-py27" + python: "2.7" + os: macos-latest + tox_env: "py27-xdist" + use_coverage: true + - name: "macos-py38" + python: "3.8" + os: macos-latest + tox_env: "py38-xdist" + use_coverage: true + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python }} on ${{ matrix.os }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox coverage + - name: Test without coverage + if: "! matrix.use_coverage" + run: "tox -e ${{ matrix.tox_env }}" + + - name: Test with coverage + if: "matrix.use_coverage" + env: + _PYTEST_TOX_COVERAGE_RUN: "coverage run -m" + COVERAGE_PROCESS_START: ".coveragerc" + _PYTEST_TOX_EXTRA_DEP: "coverage-enable-subprocess" + run: "tox -vv -e ${{ matrix.tox_env }}" + + - name: Prepare coverage token + if: (matrix.use_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' )) + run: | + python scripts/append_codecov_token.py + - name: Report coverage + if: (matrix.use_coverage) + env: + CODECOV_NAME: ${{ matrix.name }} + run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }} + + deploy: + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest' + + runs-on: ubuntu-latest + + needs: [build] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: "3.7" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade wheel setuptools tox + - name: Build package + run: | + python setup.py sdist bdist_wheel + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.pypi_token }} + - name: Publish GitHub release notes + env: + GH_RELEASE_NOTES_TOKEN: ${{ secrets.release_notes }} + run: | + sudo apt-get install pandoc + tox -e publish-gh-release-notes diff --git a/.travis.yml b/.travis.yml index 2d6883f48f7..d4f2d80483d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,12 @@ language: python dist: xenial -stages: -- baseline -- name: test - if: repo = pytest-dev/pytest AND tag IS NOT present -- name: deploy - if: repo = pytest-dev/pytest AND tag IS present -python: '3.7' +python: '3.7.4' cache: false env: global: - - PYTEST_ADDOPTS=-vv + - PYTEST_ADDOPTS="-vv --showlocals --durations=100 --exitfirst" + - PYTEST_COVERAGE=1 # setuptools-scm needs all tags in order to obtain a proper version git: @@ -22,113 +17,44 @@ install: jobs: include: - # OSX tests - first (in test stage), since they are the slower ones. - - &test-macos - os: osx - osx_image: xcode10.1 - language: generic - # Coverage for: - # - py2 with symlink in test_cmdline_python_package_symlink. - env: TOXENV=py27-xdist PYTEST_COVERAGE=1 - before_install: - - python -V - - test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 27 - - <<: *test-macos - env: TOXENV=py37-pexpect,py37-xdist PYTEST_COVERAGE=1 - before_install: - - which python3 - - python3 -V - - ln -sfn "$(which python3)" /usr/local/bin/python - - python -V - - test $(python -c 'import sys; print("%d%d" % sys.version_info[0:2])') = 37 - - # Full run of latest (major) supported versions, without xdist. - - env: TOXENV=py27 + # Coverage for: + # - TestArgComplete (linux only) + # - numpy + # - verbose=0 + - stage: baseline + env: TOXENV=py27-xdist python: '2.7' - - env: TOXENV=py37 - python: '3.7' - # Coverage tracking is slow with pypy, skip it. - - env: TOXENV=pypy-xdist - python: 'pypy' - - env: TOXENV=pypy3-xdist - python: 'pypy3' + - env: TOXENV=py38-xdist + python: '3.8' + + - stage: tests + # - _pytest.unittest._handle_skip (via pexpect). + env: TOXENV=py27-pexpect,py27-twisted + python: '2.7' - - env: TOXENV=py34-xdist - python: '3.4' - env: TOXENV=py35-xdist - python: '3.5' + python: '3.5.9' - # Coverage for: - # - pytester's LsofFdLeakChecker - # - TestArgComplete (linux only) - # - numpy - # Empty PYTEST_ADDOPTS to run this non-verbose. - - env: TOXENV=py37-lsof-numpy-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS= + - env: TOXENV=py36-xdist PYTEST_REORDER_TESTS=0 + python: '3.6.9' - # Specialized factors for py27. - - env: TOXENV=py27-nobyte-numpy-xdist - python: '2.7' - - env: TOXENV=py27-pluggymaster-xdist - python: '2.7' + - env: TOXENV=py37-numpy-pexpect-twisted + python: '3.7.4' - # Specialized factors for py37. - # Coverage for: # - test_sys_breakpoint_interception (via pexpect). - - env: TOXENV=py37-pexpect,py37-twisted PYTEST_COVERAGE=1 - - env: TOXENV=py37-pluggymaster-xdist - - env: TOXENV=py37-freeze + - env: TOXENV=py37-pexpect,py37-twisted + python: '3.7.4' - # Jobs only run via Travis cron jobs (currently daily). - - env: TOXENV=py38-xdist - python: '3.8-dev' - if: type = cron + # Run also non-verbosely, to gain coverage + - env: TOXENV=py38-xdist PYTEST_ADDOPTS="" + python: '3.8' - - stage: baseline - # Coverage for: - # - _pytest.unittest._handle_skip (via pexpect). - env: TOXENV=py27-pexpect,py27-twisted PYTEST_COVERAGE=1 - python: '2.7' - # Use py36 here for faster baseline. - - env: TOXENV=py36-xdist - python: '3.6' - - env: TOXENV=linting,docs,doctesting PYTEST_COVERAGE=1 + - env: TOXENV=linting,docs,doctesting cache: directories: - $HOME/.cache/pre-commit - - stage: deploy - python: '3.6' - install: pip install -U setuptools setuptools_scm tox - script: skip - # token to upload github release notes: GH_RELEASE_NOTES_TOKEN - env: - - secure: "OjOeL7/0JUDkV00SsTs732e8vQjHynpbG9FKTNtZZJ+1Zn4Cib+hAlwmlBnvVukML0X60YpcfjnC4quDOIGLPsh5zeXnvJmYtAIIUNQXjWz8NhcGYrhyzuP1rqV22U68RTCdmOq3lMYU/W2acwHP7T49PwJtOiUM5kF120UAQ0Zi5EmkqkIvH8oM5mO9Dlver+/U7Htpz9rhKrHBXQNCMZI6yj2aUyukqB2PN2fjAlDbCF//+FmvYw9NjT4GeFOSkTCf4ER9yfqs7yglRfwiLtOCZ2qKQhWZNsSJDB89rxIRXWavJUjJKeY2EW2/NkomYJDpqJLIF4JeFRw/HhA47CYPeo6BJqyyNV+0CovL1frpWfi9UQw2cMbgFUkUIUk3F6DD59PHNIOX2R/HX56dQsw7WKl3QuHlCOkICXYg8F7Ta684IoKjeTX03/6QNOkURfDBwfGszY0FpbxrjCSWKom6RyZdyidnESaxv9RzjcIRZVh1rp8KMrwS1OrwRSdG0zjlsPr49hWMenN/8fKgcHTV4/r1Tj6mip0dorSRCrgUNIeRBKgmui6FS8642ab5JNKOxMteVPVR2sFuhjOQ0Jy+PmvceYY9ZMWc3+/B/KVh0dZ3hwvLGZep/vxDS2PwCA5/xw31714vT5LxidKo8yECjBynMU/wUTTS695D3NY=" - addons: - apt: - packages: - # required by publish_gh_release_notes - - pandoc - after_deploy: tox -e publish_gh_release_notes - deploy: - provider: pypi - user: nicoddemus - distributions: sdist bdist_wheel - skip_upload_docs: true - password: - secure: xanTgTUu6XDQVqB/0bwJQXoDMnU5tkwZc5koz6mBkkqZhKdNOi2CLoC1XhiSZ+ah24l4V1E0GAqY5kBBcy9d7NVe4WNg4tD095LsHw+CRU6/HCVIFfyk2IZ+FPAlguesCcUiJSXOrlBF+Wj68wEvLoK7EoRFbJeiZ/f91Ww1sbtDlqXABWGHrmhPJL5Wva7o7+wG7JwJowqdZg1pbQExsCc7b53w4v2RBu3D6TJaTAzHiVsW+nUSI67vKI/uf+cR/OixsTfy37wlHgSwihYmrYLFls3V0bSpahCim3bCgMaFZx8S8xrdgJ++PzBCof2HeflFKvW+VCkoYzGEG4NrTWJoNz6ni4red9GdvfjGH3YCjAKS56h9x58zp2E5rpsb/kVq5/45xzV+dq6JRuhQ1nJWjBC6fSKAc/bfwnuFK3EBxNLkvBssLHvsNjj5XG++cB8DdS9wVGUqjpoK4puaXUWFqy4q3S9F86HEsKNgExtieA9qNx+pCIZVs6JCXZNjr0I5eVNzqJIyggNgJG6RyravsU35t9Zd9doL5g4Y7UKmAGTn1Sz24HQ4sMQgXdm2SyD8gEK5je4tlhUvfGtDvMSlstq71kIn9nRpFnqB6MFlbYSEAZmo8dGbCquoUc++6Rum208wcVbrzzVtGlXB/Ow9AbFMYeAGA0+N/K1e59c= - on: - tags: true - repo: pytest-dev/pytest - -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: - | # Do not (re-)upload coverage with cron runs. @@ -142,27 +68,13 @@ before_script: export _PYTEST_TOX_COVERAGE_RUN="coverage run -m" export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi - -script: tox +script: env COLUMNS=120 python -m tox after_success: - | if [[ "$PYTEST_COVERAGE" = 1 ]]; then - set -e - # Add last TOXENV to $PATH. - PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH" - coverage combine - coverage xml - coverage report -m - bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml -n $TOXENV-$TRAVIS_OS_NAME + env CODECOV_NAME="$TOXENV-$TRAVIS_OS_NAME" scripts/report-coverage.sh fi - -notifications: - irc: - channels: - - "chat.freenode.net#pytest" - on_success: change - on_failure: change - skip_join: true - email: - - pytest-commit@python.org +branches: + only: + - 4.6.x diff --git a/scripts/append_codecov_token.py b/scripts/append_codecov_token.py new file mode 100644 index 00000000000..b3a82075fa1 --- /dev/null +++ b/scripts/append_codecov_token.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +""" +Appends the codecov token to the 'codecov.yml' file at the root of the repository. +This is done by CI during PRs and builds on the pytest-dev repository so we can upload coverage, at least +until codecov grows some native integration like it has with Travis and AppVeyor. +See discussion in https://github.com/pytest-dev/pytest/pull/6441 for more information. +""" +import os.path +from textwrap import dedent + + +def main(): + this_dir = os.path.dirname(__file__) + cov_file = os.path.join(this_dir, "..", "codecov.yml") + + assert os.path.isfile(cov_file), "{cov_file} does not exist".format( + cov_file=cov_file + ) + + with open(cov_file, "a") as f: + # token from: https://codecov.io/gh/pytest-dev/pytest/settings + # use same URL to regenerate it if needed + text = dedent( + """ + codecov: + token: "1eca3b1f-31a2-4fb8-a8c3-138b441b50a7" + """ + ) + f.write(text) + + print("Token updated:", cov_file) + + +if __name__ == "__main__": + main() diff --git a/scripts/report-coverage.sh b/scripts/report-coverage.sh new file mode 100755 index 00000000000..fbcf20ca929 --- /dev/null +++ b/scripts/report-coverage.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [ -z "$TOXENV" ]; then + python -m pip install coverage +else + # Add last TOXENV to $PATH. + PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH" +fi + +python -m coverage combine +python -m coverage xml +python -m coverage report -m +# Set --connect-timeout to work around https://github.com/curl/curl/issues/4461 +curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh +bash codecov-upload.sh -Z -X fix -f coverage.xml "$@" diff --git a/tox.ini b/tox.ini index b788020f1c1..61cb4bef0fe 100644 --- a/tox.ini +++ b/tox.ini @@ -119,6 +119,7 @@ changedir = testing/freeze # Disable PEP 517 with pip, which does not work with PyInstaller currently. deps = pyinstaller + setuptools < 45.0.0 commands = {envpython} create_executable.py {envpython} tox_run.py From a6029ff2b70fa4e21021384c32f9f4152c09f7de Mon Sep 17 00:00:00 2001 From: Fernando Mez Date: Fri, 6 Mar 2020 12:06:34 -0300 Subject: [PATCH 71/78] BACKPORT: Introduction of Config.invocation_args --- AUTHORS | 1 + changelog/6870.feature.rst | 1 + doc/en/py27-py34-deprecation.rst | 4 +- src/_pytest/config/__init__.py | 64 ++++++++++++++++++++++++++++---- testing/test_config.py | 24 ++++++++++++ 5 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 changelog/6870.feature.rst diff --git a/AUTHORS b/AUTHORS index a19e38bbf13..80fce539294 100644 --- a/AUTHORS +++ b/AUTHORS @@ -92,6 +92,7 @@ Evan Kepner Fabien Zarifian Fabio Zadrozny Feng Ma +Fernando Mezzabotta Rey Florian Bruhin Floris Bruynooghe Gabriel Reis diff --git a/changelog/6870.feature.rst b/changelog/6870.feature.rst new file mode 100644 index 00000000000..e2f365a3317 --- /dev/null +++ b/changelog/6870.feature.rst @@ -0,0 +1 @@ +New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. diff --git a/doc/en/py27-py34-deprecation.rst b/doc/en/py27-py34-deprecation.rst index 95e96de0456..0ca97539c82 100644 --- a/doc/en/py27-py34-deprecation.rst +++ b/doc/en/py27-py34-deprecation.rst @@ -17,9 +17,9 @@ are available on PyPI. While pytest ``5.0`` will be the new mainstream and development version, until **January 2020** the pytest core team plans to make bug-fix releases of the pytest ``4.6`` series by -back-porting patches to the ``4.6-maintenance`` branch that affect Python 2 users. +back-porting patches to the ``4.6.x`` branch that affect Python 2 users. -**After 2020**, the core team will no longer actively backport patches, but the ``4.6-maintenance`` +**After 2020**, the core team will no longer actively backport patches, but the ``4.6.x`` branch will continue to exist so the community itself can contribute patches. The core team will be happy to accept those patches and make new ``4.6`` releases **until mid-2020**. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 63d12104060..0737ff9d51c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -13,6 +13,7 @@ import types import warnings +import attr import py import six from packaging.version import Version @@ -35,6 +36,7 @@ from _pytest.compat import safe_str from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import Path from _pytest.warning_types import PytestConfigWarning hookimpl = HookimplMarker("pytest") @@ -154,10 +156,15 @@ def directory_arg(path, optname): builtin_plugins.add("pytester") -def get_config(args=None): +def get_config(args=None, plugins=None): # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - config = Config(pluginmanager) + config = Config( + pluginmanager, + invocation_params=Config.InvocationParams( + args=args, plugins=plugins, dir=Path().resolve() + ), + ) if args is not None: # Handle any "-p no:plugin" args. @@ -190,7 +197,7 @@ def _prepareconfig(args=None, plugins=None): msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) - config = get_config(args) + config = get_config(args, plugins) pluginmanager = config.pluginmanager try: if plugins: @@ -686,13 +693,52 @@ def _iter_rewritable_modules(package_files): class Config(object): - """ access to configuration values, pluginmanager and plugin hooks. """ + """ + Access to configuration values, pluginmanager and plugin hooks. + + :ivar PytestPluginManager pluginmanager: the plugin manager handles plugin registration and hook invocation. + + :ivar argparse.Namespace option: access to command line option as attributes. + + :ivar InvocationParams invocation_params: + + Object containing the parameters regarding the ``pytest.main`` + invocation. + Contains the followinig read-only attributes: + * ``args``: list of command-line arguments as passed to ``pytest.main()``. + * ``plugins``: list of extra plugins, might be None + * ``dir``: directory where ``pytest.main()`` was invoked from. + """ + + @attr.s(frozen=True) + class InvocationParams(object): + """Holds parameters passed during ``pytest.main()`` + + .. note:: + + Currently the environment variable PYTEST_ADDOPTS is also handled by + pytest implicitly, not being part of the invocation. + + Plugins accessing ``InvocationParams`` must be aware of that. + """ + + args = attr.ib() + plugins = attr.ib() + dir = attr.ib() + + def __init__(self, pluginmanager, invocation_params=None, *args): + from .argparsing import Parser, FILE_OR_DIR + + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=Path().resolve() + ) - def __init__(self, pluginmanager): #: access to command line option as attributes. #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead self.option = argparse.Namespace() - from .argparsing import Parser, FILE_OR_DIR + + self.invocation_params = invocation_params _a = FILE_OR_DIR self._parser = Parser( @@ -709,9 +755,13 @@ def __init__(self, pluginmanager): self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False - self.invocation_dir = py.path.local() self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) + @property + def invocation_dir(self): + """Backward compatibility""" + return py.path.local(str(self.invocation_params.dir)) + def add_cleanup(self, func): """ Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" diff --git a/testing/test_config.py b/testing/test_config.py index d0e5937303f..d13f119b020 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -19,6 +19,7 @@ from _pytest.main import EXIT_OK from _pytest.main import EXIT_TESTSFAILED from _pytest.main import EXIT_USAGEERROR +from _pytest.pathlib import Path class TestParseIni(object): @@ -1222,6 +1223,29 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): assert result.ret == EXIT_USAGEERROR +def test_invocation_args(testdir): + """Ensure that Config.invocation_* arguments are correctly defined""" + + class DummyPlugin(object): + pass + + p = testdir.makepyfile("def test(): pass") + plugin = DummyPlugin() + rec = testdir.inline_run(p, "-v", plugins=[plugin]) + calls = rec.getcalls("pytest_runtest_protocol") + assert len(calls) == 1 + call = calls[0] + config = call.item.config + + assert config.invocation_params.args == [p, "-v"] + assert config.invocation_params.dir == Path(str(testdir.tmpdir)) + + plugins = config.invocation_params.plugins + assert len(plugins) == 2 + assert plugins[0] is plugin + assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() + + @pytest.mark.parametrize( "plugin", [ From 2a5ca51fe8343b19708114deefb4fd43c805b48f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 7 May 2020 17:42:57 -0300 Subject: [PATCH 72/78] [4.6] Merge pull request #7179 from asottile/py39 --- setup.cfg | 2 ++ testing/code/test_source.py | 2 +- tox.ini | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 9d0aa332e94..368df1e1ac0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,8 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 platforms = unix, linux, osx, cygwin, win32 [options] diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 0a2a98c0202..d2e3c134cd5 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -501,7 +501,7 @@ class A(object): class B(object): pass - B.__name__ = "B2" + B.__name__ = B.__qualname__ = "B2" assert getfslineno(B)[1] == -1 diff --git a/tox.ini b/tox.ini index 61cb4bef0fe..f75a0610482 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ envlist = py36 py37 py38 + py39 pypy pypy3 {py27,py37}-{pexpect,xdist,twisted,numpy,pluggymaster} From f1d7aa60b10420674149b935e4b2388f6e08e657 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 May 2020 11:14:53 -0400 Subject: [PATCH 73/78] Preparing release version 4.6.10 --- CHANGELOG.rst | 16 ++++++++++++++++ changelog/6404.trivial.rst | 1 - changelog/6870.feature.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.10.rst | 20 ++++++++++++++++++++ doc/en/example/parametrize.rst | 7 ++++--- doc/en/example/reportingdemo.rst | 4 ++-- doc/en/getting-started.rst | 2 +- 8 files changed, 44 insertions(+), 8 deletions(-) delete mode 100644 changelog/6404.trivial.rst delete mode 100644 changelog/6870.feature.rst create mode 100644 doc/en/announce/release-4.6.10.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4305c742904..3992b2b5f8f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,22 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.10 (2020-05-08) +========================== + +Features +-------- + +- `#6870 `_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. + + + +Trivial/Internal Changes +------------------------ + +- `#6404 `_: Remove usage of ``parser`` module, deprecated in Python 3.9. + + pytest 4.6.9 (2020-01-04) ========================= diff --git a/changelog/6404.trivial.rst b/changelog/6404.trivial.rst deleted file mode 100644 index 8252098b6a2..00000000000 --- a/changelog/6404.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Remove usage of ``parser`` module, deprecated in Python 3.9. diff --git a/changelog/6870.feature.rst b/changelog/6870.feature.rst deleted file mode 100644 index e2f365a3317..00000000000 --- a/changelog/6870.feature.rst +++ /dev/null @@ -1 +0,0 @@ -New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 4cfd9fa0ffa..ab3bbad5b84 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.10 release-4.6.9 release-4.6.8 release-4.6.7 diff --git a/doc/en/announce/release-4.6.10.rst b/doc/en/announce/release-4.6.10.rst new file mode 100644 index 00000000000..57938b8751a --- /dev/null +++ b/doc/en/announce/release-4.6.10.rst @@ -0,0 +1,20 @@ +pytest-4.6.10 +======================================= + +pytest 4.6.10 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Fernando Mez + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index e3fd4564dec..2ab5e3ab10c 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -434,10 +434,11 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ......sss......ssssssssssss [100%] + ...ssssssssssssssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.5' not found - 12 passed, 15 skipped in 0.12 seconds + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.5' not found + 3 passed, 24 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 928c365cafc..d00c9836265 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -436,7 +436,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: items = [1, 2, 3] print("items is %r" % items) > a, b = items.pop() - E TypeError: 'int' object is not iterable + E TypeError: cannot unpack non-iterable int object failure_demo.py:182: TypeError --------------------------- Captured stdout call --------------------------- @@ -515,7 +515,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_z2_type_error(self): items = 3 > a, b = items - E TypeError: 'int' object is not iterable + E TypeError: cannot unpack non-iterable int object failure_demo.py:222: TypeError ______________________ TestMoreErrors.test_startswith ______________________ diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index d6c62cbe8c0..882385b2960 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py + This is pytest version 4.x.y, imported from $PYTHON_PREFIX/lib/python3.7/site-packages/pytest.py .. _`simpletest`: From 180f93158e380fd570340b39ae5aafc066e07ff8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 May 2020 12:59:26 -0300 Subject: [PATCH 74/78] Introduce missing remark as commented in original PR https://github.com/pytest-dev/pytest/pull/6870/files#r390667966 --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3992b2b5f8f..1bef283ef3c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,7 +26,7 @@ Features - `#6870 `_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. - + Remark: while this is technically a new feature and according to our `policy `_ it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix. Trivial/Internal Changes ------------------------ From 049f5b513a59723345b811aa61f604fb0ab7f8d9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 13 Dec 2019 07:47:48 -0300 Subject: [PATCH 75/78] Make 'S' and 'F' aliases to 's' and 'f' respectively (#6337) (cherry picked from commit ecd1e43afbda6a8c9d20cf1979b3b6297fb1572a) --- changelog/6334.bugfix.rst | 3 +++ src/_pytest/terminal.py | 19 ++++++++++++------- testing/test_terminal.py | 39 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 changelog/6334.bugfix.rst diff --git a/changelog/6334.bugfix.rst b/changelog/6334.bugfix.rst new file mode 100644 index 00000000000..abd4c748b4c --- /dev/null +++ b/changelog/6334.bugfix.rst @@ -0,0 +1,3 @@ +Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``). + +The upper case variants were never documented and the preferred form should be the lower case. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index eb1970d5103..4418338c656 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -166,7 +166,11 @@ def getreportopt(config): reportchars += "w" elif config.option.disable_warnings and "w" in reportchars: reportchars = reportchars.replace("w", "") + aliases = {"F", "S"} for char in reportchars: + # handle old aliases + if char in aliases: + char = char.lower() if char == "a": reportopts = "sxXwEf" elif char == "A": @@ -179,15 +183,18 @@ def getreportopt(config): @pytest.hookimpl(trylast=True) # after _pytest.runner def pytest_report_teststatus(report): + letter = "F" if report.passed: letter = "." elif report.skipped: letter = "s" - elif report.failed: - letter = "F" - if report.when != "call": - letter = "f" - return report.outcome, letter, report.outcome.upper() + + outcome = report.outcome + if report.when in ("collect", "setup", "teardown") and outcome == "failed": + outcome = "error" + letter = "E" + + return outcome, letter, outcome.upper() @attr.s @@ -935,9 +942,7 @@ def show_skipped(lines): "x": show_xfailed, "X": show_xpassed, "f": partial(show_simple, "failed"), - "F": partial(show_simple, "failed"), "s": show_skipped, - "S": show_skipped, "p": partial(show_simple, "passed"), "E": partial(show_simple, "error"), } diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1b2e46c7c2b..752c894ca93 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -759,6 +759,35 @@ def test(i): result = testdir.runpytest(*params) result.stdout.fnmatch_lines(["collected 3 items", "hello from hook: 3 items"]) + def test_summary_f_alias(self, testdir): + """Test that 'f' and 'F' report chars are aliases and don't show up twice in the summary (#6334)""" + testdir.makepyfile( + """ + def test(): + assert False + """ + ) + result = testdir.runpytest("-rfF") + expected = "FAILED test_summary_f_alias.py::test - assert False" + result.stdout.fnmatch_lines([expected]) + assert result.stdout.lines.count(expected) == 1 + + def test_summary_s_alias(self, testdir): + """Test that 's' and 'S' report chars are aliases and don't show up twice in the summary""" + testdir.makepyfile( + """ + import pytest + + @pytest.mark.skip + def test(): + pass + """ + ) + result = testdir.runpytest("-rsS") + expected = "SKIPPED [1] test_summary_s_alias.py:3: unconditional skip" + result.stdout.fnmatch_lines([expected]) + assert result.stdout.lines.count(expected) == 1 + def test_fail_extra_reporting(testdir, monkeypatch): monkeypatch.setenv("COLUMNS", "80") @@ -1551,12 +1580,16 @@ def test_teardown_with_test_also_failing( testdir.makepyfile( """ def test_foo(fail_teardown): - assert False + assert 0 """ ) - output = testdir.runpytest() + output = testdir.runpytest("-rfE") output.stdout.re_match_lines( - [r"test_teardown_with_test_also_failing.py FE\s+\[100%\]"] + [ + r"test_teardown_with_test_also_failing.py FE\s+\[100%\]", + "FAILED test_teardown_with_test_also_failing.py::test_foo - assert 0", + "ERROR test_teardown_with_test_also_failing.py::test_foo - assert False", + ] ) def test_teardown_many(self, testdir, many_files): From 1c465bd32fc92041f69b9e0ec8884f14121acd8b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Wed, 3 Jun 2020 16:55:09 +0200 Subject: [PATCH 76/78] Add a change note on issue #7310 --- changelog/7310.bugfix.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 changelog/7310.bugfix.rst diff --git a/changelog/7310.bugfix.rst b/changelog/7310.bugfix.rst new file mode 100644 index 00000000000..9dab65c5df5 --- /dev/null +++ b/changelog/7310.bugfix.rst @@ -0,0 +1,9 @@ +Fix ``UnboundLocalError: local variable 'letter' referenced before +assignment`` in ``_pytest.terminal.pytest_report_teststatus()`` +when plugins return report objects in an unconventional state. + +This was making ``pytest_report_teststatus()`` skip +entering if-block branches that declare the ``letter`` variable. + +The fix was to set the initial value of the ``letter`` before +the if-block cascade so that it always has a value. From 2262734edfce27bed7839c93a9f7c42ace056a70 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 4 Jun 2020 13:49:08 -0400 Subject: [PATCH 77/78] Preparing release version 4.6.11 --- CHANGELOG.rst | 22 ++++++++++++++++++++++ changelog/6334.bugfix.rst | 3 --- changelog/7310.bugfix.rst | 9 --------- doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.11.rst | 20 ++++++++++++++++++++ doc/en/example/simple.rst | 2 +- 6 files changed, 44 insertions(+), 13 deletions(-) delete mode 100644 changelog/6334.bugfix.rst delete mode 100644 changelog/7310.bugfix.rst create mode 100644 doc/en/announce/release-4.6.11.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1bef283ef3c..89437663f59 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,28 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.11 (2020-06-04) +========================== + +Bug Fixes +--------- + +- `#6334 `_: Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``). + + The upper case variants were never documented and the preferred form should be the lower case. + + +- `#7310 `_: Fix ``UnboundLocalError: local variable 'letter' referenced before + assignment`` in ``_pytest.terminal.pytest_report_teststatus()`` + when plugins return report objects in an unconventional state. + + This was making ``pytest_report_teststatus()`` skip + entering if-block branches that declare the ``letter`` variable. + + The fix was to set the initial value of the ``letter`` before + the if-block cascade so that it always has a value. + + pytest 4.6.10 (2020-05-08) ========================== diff --git a/changelog/6334.bugfix.rst b/changelog/6334.bugfix.rst deleted file mode 100644 index abd4c748b4c..00000000000 --- a/changelog/6334.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix summary entries appearing twice when ``f/F`` and ``s/S`` report chars were used at the same time in the ``-r`` command-line option (for example ``-rFf``). - -The upper case variants were never documented and the preferred form should be the lower case. diff --git a/changelog/7310.bugfix.rst b/changelog/7310.bugfix.rst deleted file mode 100644 index 9dab65c5df5..00000000000 --- a/changelog/7310.bugfix.rst +++ /dev/null @@ -1,9 +0,0 @@ -Fix ``UnboundLocalError: local variable 'letter' referenced before -assignment`` in ``_pytest.terminal.pytest_report_teststatus()`` -when plugins return report objects in an unconventional state. - -This was making ``pytest_report_teststatus()`` skip -entering if-block branches that declare the ``letter`` variable. - -The fix was to set the initial value of the ``letter`` before -the if-block cascade so that it always has a value. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index ab3bbad5b84..74ad0285509 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.11 release-4.6.10 release-4.6.9 release-4.6.8 diff --git a/doc/en/announce/release-4.6.11.rst b/doc/en/announce/release-4.6.11.rst new file mode 100644 index 00000000000..276584bdf52 --- /dev/null +++ b/doc/en/announce/release-4.6.11.rst @@ -0,0 +1,20 @@ +pytest-4.6.11 +======================================= + +pytest 4.6.11 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Sviatoslav Sydorenko + + +Happy testing, +The pytest Development Team diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 140f4b840f1..1dee981be77 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -440,7 +440,7 @@ Now we can profile which test functions execute the slowest: test_some_are_slow.py ... [100%] ========================= slowest 3 test durations ========================= - 0.30s call test_some_are_slow.py::test_funcslow2 + 0.31s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.12 seconds ========================= From 90aaeebc8e699f04e7f9a5a4306a954029adacfc Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 25 Oct 2020 01:08:12 +0200 Subject: [PATCH 78/78] testing: python 3.10 fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Miro Hrončok --- testing/test_runner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/test_runner.py b/testing/test_runner.py index 6906efb910c..804c2831c89 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -336,8 +336,10 @@ def test_method(self): assert reps[2].failed assert reps[2].when == "teardown" assert reps[2].longrepr.reprcrash.message in ( - # python3 error + # python3 < 3.10 error "TypeError: teardown_method() missing 2 required positional arguments: 'y' and 'z'", + # python3 >= 3.10 error + "TypeError: TestClass.teardown_method() missing 2 required positional arguments: 'y' and 'z'", # python2 error "TypeError: teardown_method() takes exactly 4 arguments (2 given)", )