From 87e4a28351ac372f553b9fe588da37cb35aa1b81 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 3 Jun 2017 18:26:34 -0300 Subject: [PATCH 01/21] Fix incorrect collected items report when specifying tests on the command-line Fix #2464 --- _pytest/main.py | 6 +++++- changelog/2464.bugfix | 1 + testing/acceptance_test.py | 4 ++-- testing/test_collection.py | 19 ++++++++++++++----- 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 changelog/2464.bugfix diff --git a/_pytest/main.py b/_pytest/main.py index 480810cc8..ec4ec2cc7 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -763,7 +763,11 @@ class Session(FSCollector): if not has_matched and len(rep.result) == 1 and x.name == "()": nextnames.insert(0, name) resultnodes.extend(self.matchnodes([x], nextnames)) - node.ihook.pytest_collectreport(report=rep) + else: + # report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134) + node.ihook.pytest_collectreport(report=rep) return resultnodes def genitems(self, node): diff --git a/changelog/2464.bugfix b/changelog/2464.bugfix new file mode 100644 index 000000000..12062fd9e --- /dev/null +++ b/changelog/2464.bugfix @@ -0,0 +1 @@ +Fix incorrect "collected items" report when specifying tests on the command-line. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 00abfc38d..9cb2650de 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -317,8 +317,8 @@ class TestGeneralUsage(object): ]) assert 'sessionstarttime' not in result.stderr.str() - @pytest.mark.parametrize('lookfor', ['test_fun.py', 'test_fun.py::test_a']) - def test_issue134_report_syntaxerror_when_collecting_member(self, testdir, lookfor): + @pytest.mark.parametrize('lookfor', ['test_fun.py::test_a']) + def test_issue134_report_error_when_collecting_member(self, testdir, lookfor): testdir.makepyfile(test_fun=""" def test_a(): pass diff --git a/testing/test_collection.py b/testing/test_collection.py index c19fc0e72..a90269789 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -369,6 +369,11 @@ class TestSession(object): assert len(colitems) == 1 assert colitems[0].fspath == p + def get_reported_items(self, hookrec): + """Return pytest.Item instances reported by the pytest_collectreport hook""" + calls = hookrec.getcalls('pytest_collectreport') + return [x for call in calls for x in call.report.result + if isinstance(x, pytest.Item)] def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -386,9 +391,10 @@ class TestSession(object): ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), - ("pytest_collectreport", "report.nodeid == ''") + ("pytest_collectreport", "report.result[0].name == 'test_func'"), ]) + # ensure we are reporting the collection of the single test item (#2464) + assert [x.name for x in self.get_reported_items(hookrec)] == ['test_func'] def test_collect_protocol_method(self, testdir): p = testdir.makepyfile(""" @@ -407,6 +413,8 @@ class TestSession(object): assert items[0].name == "test_method" newid = items[0].nodeid assert newid == normid + # 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'] def test_collect_custom_nodes_multi_id(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -436,9 +444,8 @@ class TestSession(object): "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), - #("pytest_collectreport", - # "report.fspath == %r" % str(rcol.fspath)), ]) + assert len(self.get_reported_items(hookrec)) == 2 def test_collect_subdir_event_ordering(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -495,11 +502,13 @@ class TestSession(object): def test_method(self): pass """) - arg = p.basename + ("::TestClass::test_method") + arg = p.basename + "::TestClass::test_method" items, hookrec = testdir.inline_genitems(arg) assert len(items) == 1 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'] class Test_getinitialnodes(object): def test_global_file(self, testdir, tmpdir): From 46d157fe07c4b85c9f9ccefcb3d6e3309b0ad922 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 3 Jun 2017 18:42:26 -0300 Subject: [PATCH 02/21] Fix collection report when collecting a single test item --- _pytest/terminal.py | 2 +- testing/test_runner.py | 4 ++-- testing/test_terminal.py | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index e226d607b..af89d0fc2 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -282,7 +282,7 @@ class TerminalReporter: line = "collected " else: line = "collecting " - line += str(self._numcollected) + " items" + line += str(self._numcollected) + " item" + ('' if self._numcollected == 1 else 's') if errors: line += " / %d errors" % errors if skipped: diff --git a/testing/test_runner.py b/testing/test_runner.py index 51d430fc8..def80ea5f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -513,12 +513,12 @@ def test_pytest_no_tests_collected_exit_status(testdir): assert 1 """) result = testdir.runpytest() - result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*collected 1 item*') result.stdout.fnmatch_lines('*1 passed*') assert result.ret == main.EXIT_OK result = testdir.runpytest('-k nonmatch') - result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*collected 1 item*') result.stdout.fnmatch_lines('*1 deselected*') assert result.ret == main.EXIT_NOTESTSCOLLECTED diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 5a90b3dd4..45c354206 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -204,6 +204,15 @@ class TestTerminal(object): assert result.ret == 2 result.stdout.fnmatch_lines(['*KeyboardInterrupt*']) + def test_collect_single_item(self, testdir): + """Use singular 'item' when reporting a single test item""" + testdir.makepyfile(""" + def test_foobar(): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['collected 1 item']) + class TestCollectonly(object): def test_collectonly_basic(self, testdir): From 620ba5971f7c44b36f1c6b62d70c89914c6a071e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 6 Jun 2017 22:25:15 -0300 Subject: [PATCH 03/21] deprecated_call context manager captures warnings already raised Fix #2469 --- _pytest/recwarn.py | 62 ++++++++++++++++++++++------------------- changelog/2469.bugfix | 4 +++ testing/test_recwarn.py | 58 +++++++++++++++++++++++++++++++------- 3 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 changelog/2469.bugfix diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 7ad6fef89..36b22e940 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -27,10 +27,8 @@ def recwarn(): def deprecated_call(func=None, *args, **kwargs): - """ assert that calling ``func(*args, **kwargs)`` triggers a - ``DeprecationWarning`` or ``PendingDeprecationWarning``. - - This function can be used as a context manager:: + """context manager that can be used to ensure a block of code triggers a + ``DeprecationWarning`` or ``PendingDeprecationWarning``:: >>> import warnings >>> def api_call_v2(): @@ -40,38 +38,46 @@ def deprecated_call(func=None, *args, **kwargs): >>> with deprecated_call(): ... assert api_call_v2() == 200 - Note: we cannot use WarningsRecorder here because it is still subject - to the mechanism that prevents warnings of the same type from being - triggered twice for the same module. See #1190. + ``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``, + in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings + types above. """ if not func: - return WarningsChecker(expected_warning=(DeprecationWarning, PendingDeprecationWarning)) + return _DeprecatedCallContext() + else: + with _DeprecatedCallContext(): + return func(*args, **kwargs) - categories = [] - def warn_explicit(message, category, *args, **kwargs): - categories.append(category) +class _DeprecatedCallContext(object): + """Implements the logic to capture deprecation warnings as a context manager.""" - def warn(message, category=None, *args, **kwargs): + def __enter__(self): + self._captured_categories = [] + self._old_warn = warnings.warn + self._old_warn_explicit = warnings.warn_explicit + warnings.warn_explicit = self._warn_explicit + warnings.warn = self._warn + + def _warn_explicit(self, message, category, *args, **kwargs): + self._captured_categories.append(category) + + def _warn(self, message, category=None, *args, **kwargs): if isinstance(message, Warning): - categories.append(message.__class__) + self._captured_categories.append(message.__class__) else: - categories.append(category) + self._captured_categories.append(category) - old_warn = warnings.warn - old_warn_explicit = warnings.warn_explicit - warnings.warn_explicit = warn_explicit - warnings.warn = warn - try: - ret = func(*args, **kwargs) - finally: - warnings.warn_explicit = old_warn_explicit - warnings.warn = old_warn - deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) - if not any(issubclass(c, deprecation_categories) for c in categories): - __tracebackhide__ = True - raise AssertionError("%r did not produce DeprecationWarning" % (func,)) - return ret + def __exit__(self, exc_type, exc_val, exc_tb): + warnings.warn_explicit = self._old_warn_explicit + warnings.warn = self._old_warn + + if exc_type is None: + deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) + if not any(issubclass(c, deprecation_categories) for c in self._captured_categories): + __tracebackhide__ = True + msg = "Did not produce DeprecationWarning or PendingDeprecationWarning" + raise AssertionError(msg) def warns(expected_warning, *args, **kwargs): diff --git a/changelog/2469.bugfix b/changelog/2469.bugfix new file mode 100644 index 000000000..492c62e08 --- /dev/null +++ b/changelog/2469.bugfix @@ -0,0 +1,4 @@ +``deprecated_call`` in context-manager form now captures deprecation warnings even if +the same warning has already been raised. Also, ``deprecated_call`` will always produce +the same error message (previously it would produce different messages in context-manager vs. +function-call mode). diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 75dacc040..f1048f07d 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -77,7 +77,7 @@ class TestDeprecatedCall(object): def test_deprecated_call_raises(self): with pytest.raises(AssertionError) as excinfo: pytest.deprecated_call(self.dep, 3, 5) - assert str(excinfo).find("did not produce") != -1 + assert 'Did not produce' in str(excinfo) def test_deprecated_call(self): pytest.deprecated_call(self.dep, 0, 5) @@ -106,31 +106,69 @@ class TestDeprecatedCall(object): pytest.deprecated_call(self.dep_explicit, 0) pytest.deprecated_call(self.dep_explicit, 0) - def test_deprecated_call_as_context_manager_no_warning(self): - with pytest.raises(pytest.fail.Exception, matches='^DID NOT WARN'): - with pytest.deprecated_call(): - self.dep(1) + @pytest.mark.parametrize('mode', ['context_manager', 'call']) + def test_deprecated_call_no_warning(self, mode): + """Ensure deprecated_call() raises the expected failure when its block/function does + not raise a deprecation warning. + """ + def f(): + pass + + msg = 'Did not produce DeprecationWarning or PendingDeprecationWarning' + with pytest.raises(AssertionError, matches=msg): + if mode == 'call': + pytest.deprecated_call(f) + else: + with pytest.deprecated_call(): + f() @pytest.mark.parametrize('warning_type', [PendingDeprecationWarning, DeprecationWarning]) @pytest.mark.parametrize('mode', ['context_manager', 'call']) - def test_deprecated_call_modes(self, warning_type, mode): + @pytest.mark.parametrize('call_f_first', [True, False]) + def test_deprecated_call_modes(self, warning_type, mode, call_f_first): + """Ensure deprecated_call() captures a deprecation warning as expected inside its + block/function. + """ def f(): warnings.warn(warning_type("hi")) - + return 10 + + # ensure deprecated_call() can capture the warning even if it has already been triggered + if call_f_first: + assert f() == 10 if mode == 'call': - pytest.deprecated_call(f) + assert pytest.deprecated_call(f) == 10 else: with pytest.deprecated_call(): - f() + assert f() == 10 + + @pytest.mark.parametrize('mode', ['context_manager', 'call']) + def test_deprecated_call_exception_is_raised(self, mode): + """If the block of the code being tested by deprecated_call() raises an exception, + it must raise the exception undisturbed. + """ + def f(): + raise ValueError('some exception') + + with pytest.raises(ValueError, match='some exception'): + if mode == 'call': + pytest.deprecated_call(f) + else: + with pytest.deprecated_call(): + f() def test_deprecated_call_specificity(self): other_warnings = [Warning, UserWarning, SyntaxWarning, RuntimeWarning, FutureWarning, ImportWarning, UnicodeWarning] for warning in other_warnings: def f(): - py.std.warnings.warn(warning("hi")) + warnings.warn(warning("hi")) + with pytest.raises(AssertionError): pytest.deprecated_call(f) + with pytest.raises(AssertionError): + with pytest.deprecated_call(): + f() def test_deprecated_function_already_called(self, testdir): """deprecated_call should be able to catch a call to a deprecated From 8bb589fc5dc4eafffe051d6a665c18d8c5f8c0d2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 9 Jun 2017 18:47:07 -0300 Subject: [PATCH 04/21] Fix internal error when trying to detect the start of a recursive traceback. Fix #2486 --- _pytest/_code/code.py | 7 +++++-- changelog/2486.bugfix | 1 + testing/code/test_excinfo.py | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 changelog/2486.bugfix diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 9b3408dc4..5b7cc4191 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -640,8 +640,11 @@ class FormattedExcinfo(object): ).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback)) traceback = traceback[:max_frames] + traceback[-max_frames:] else: - extraline = "!!! Recursion detected (same locals & position)" - traceback = traceback[:recursionindex + 1] + if recursionindex is not None: + extraline = "!!! Recursion detected (same locals & position)" + traceback = traceback[:recursionindex + 1] + else: + extraline = None return traceback, extraline diff --git a/changelog/2486.bugfix b/changelog/2486.bugfix new file mode 100644 index 000000000..97917197c --- /dev/null +++ b/changelog/2486.bugfix @@ -0,0 +1 @@ +Fix internal error when trying to detect the start of a recursive traceback. diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 3128beff8..0b074d64a 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function +import sys import operator import _pytest import py @@ -1173,3 +1174,25 @@ def test_exception_repr_extraction_error_on_recursion(): '*The following exception happened*', '*ValueError: The truth value of an array*', ]) + + +def test_no_recursion_index_on_recursion_error(): + """ + Ensure that we don't break in case we can't find the recursion index + during a recursion error (#2486). + """ + try: + class RecursionDepthError(object): + def __getattr__(self, attr): + return getattr(self, '_' + attr) + + RecursionDepthError().trigger + except: + from _pytest._code.code import ExceptionInfo + exc_info = ExceptionInfo() + if sys.version_info[:2] == (2, 6): + assert "'RecursionDepthError' object has no attribute '___" in str(exc_info.getrepr()) + else: + assert 'maximum recursion' in str(exc_info.getrepr()) + else: + assert 0 From 536f1723acfcf81a1d6bd8b25cc107294388c841 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Jun 2017 12:25:40 -0300 Subject: [PATCH 05/21] Add issue links in the CHANGELOG entries This unfortunately no longer supports multiple entries with the same text, but this is worth the improved readability and navigation IMO --- changelog/_template.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/_template.rst b/changelog/_template.rst index 66fd6ae56..4f1d46d80 100644 --- a/changelog/_template.rst +++ b/changelog/_template.rst @@ -13,7 +13,7 @@ {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category]|dictsort(by='value') %} -- {{ text }}{% if category != 'vendor' %} ({{ values|sort|join(', ') }}){% endif %} +- {{ text }}{% if category != 'vendor' %} (`{{ values[0] }} `_){% endif %} {% endfor %} From f0541b685bb7d27ea87eb8fd8bddbd0280a9b48d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Jun 2017 12:31:20 -0300 Subject: [PATCH 06/21] Improve CHANGELOG formatting a bit --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34fee3328..87af5d72a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,9 +25,9 @@ Bug Fixes - ``UnicodeWarning`` is issued from the internal pytest warnings plugin only when the message contains non-ascii unicode (Python 2 only). (#2463) -- Added a workaround for Python 3.6 WindowsConsoleIO breaking due to Pytests's - FDCapture. Other code using console handles might still be affected by the - very same issue and might require further workarounds/fixes, i.e. colorama. +- Added a workaround for Python 3.6 ``WindowsConsoleIO`` breaking due to Pytests's + ``FDCapture``. Other code using console handles might still be affected by the + very same issue and might require further workarounds/fixes, i.e. ``colorama``. (#2467) From 5a856b6e29172cb9f38e7e8c5413e13684307ec0 Mon Sep 17 00:00:00 2001 From: Ryan Fitzpatrick Date: Mon, 12 Jun 2017 20:39:42 -0400 Subject: [PATCH 07/21] handle and reraise subrequest finalizer exceptions --- _pytest/fixtures.py | 10 ++++++++-- changelog/2440.bugfix | 1 + testing/python/fixture.py | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 changelog/2440.bugfix diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 1a6e245c7..5799b00e6 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -733,10 +733,16 @@ class FixtureDef: self._finalizer.append(finalizer) def finish(self): + exceptions = [] try: while self._finalizer: - func = self._finalizer.pop() - func() + try: + func = self._finalizer.pop() + func() + except: + exceptions.append(sys.exc_info()) + if exceptions: + py.builtin._reraise(*exceptions[0]) finally: ihook = self._fixturemanager.session.ihook ihook.pytest_fixture_post_finalizer(fixturedef=self) diff --git a/changelog/2440.bugfix b/changelog/2440.bugfix new file mode 100644 index 000000000..6f64d1dee --- /dev/null +++ b/changelog/2440.bugfix @@ -0,0 +1 @@ +Exceptions in a SubRequest's finish() block are suppressed until all finalizers are called, with the initial exception reraised. \ No newline at end of file diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 4c9ad7a91..ca2e078bd 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -657,6 +657,32 @@ class TestRequestBasic(object): "*1 error*" # XXX the whole module collection fails ]) + def test_request_subrequest_addfinalizer_exceptions(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + def _excepts(): + raise Exception('Error') + @pytest.fixture + def subrequest(request): + return request + @pytest.fixture + def something(subrequest): + subrequest.addfinalizer(lambda: l.append(1)) + subrequest.addfinalizer(lambda: l.append(2)) + subrequest.addfinalizer(_excepts) + @pytest.fixture + def excepts(subrequest): + subrequest.addfinalizer(_excepts) + subrequest.addfinalizer(lambda: l.append(3)) + def test_first(something, excepts): + pass + def test_second(): + assert l == [3, 2, 1] + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, failed=1) + def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") item, = testdir.genitems([modcol]) From 21137cf8c51e1ac7252a609fdf9804b4efae208f Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Mon, 12 Jun 2017 19:45:35 -0700 Subject: [PATCH 08/21] Add firstresult=True to the hook docs --- _pytest/hookspec.py | 59 ++++++++++++++++++++++++++++---------- doc/en/writing_plugins.rst | 6 ++-- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index e96fe32cd..2c9a66163 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -73,7 +73,9 @@ def pytest_configure(config): @hookspec(firstresult=True) def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. """ + """return initialized config object, parsing the specified args. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_cmdline_preparse(config, args): """(deprecated) modify command line arguments before option parsing. """ @@ -81,7 +83,9 @@ def pytest_cmdline_preparse(config, args): @hookspec(firstresult=True) def pytest_cmdline_main(config): """ called for performing the main command line action. The default - implementation will invoke the configure hooks and runtest_mainloop. """ + implementation will invoke the configure hooks and runtest_mainloop. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_load_initial_conftests(early_config, parser, args): """ implements the loading of initial conftest files ahead @@ -94,7 +98,9 @@ def pytest_load_initial_conftests(early_config, parser, args): @hookspec(firstresult=True) def pytest_collection(session): - """ perform the collection protocol for the given session. """ + """ perform the collection protocol for the given session. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_collection_modifyitems(session, config, items): """ called after collection has been performed, may filter or re-order @@ -108,11 +114,15 @@ def pytest_ignore_collect(path, config): """ return True to prevent considering this path for collection. This hook is consulted for all files and directories prior to calling more specific hooks. + + Stops at first non-None result, see :ref:`firstresult` """ @hookspec(firstresult=True) def pytest_collect_directory(path, parent): - """ called before traversing a directory for collection files. """ + """ called before traversing a directory for collection files. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_collect_file(path, parent): """ return collection Node or None for the given path. Any new node @@ -133,7 +143,9 @@ def pytest_deselected(items): @hookspec(firstresult=True) def pytest_make_collect_report(collector): - """ perform ``collector.collect()`` and return a CollectReport. """ + """ perform ``collector.collect()`` and return a CollectReport. + + Stops at first non-None result, see :ref:`firstresult` """ # ------------------------------------------------------------------------- # Python test function related hooks @@ -145,15 +157,20 @@ def pytest_pycollect_makemodule(path, parent): This hook will be called for each matching test module path. The pytest_collect_file hook needs to be used if you want to create test modules for files that do not match as a test module. - """ + + Stops at first non-None result, see :ref:`firstresult` """ @hookspec(firstresult=True) def pytest_pycollect_makeitem(collector, name, obj): - """ return custom item/collector for a python object in a module, or None. """ + """ return custom item/collector for a python object in a module, or None. + + Stops at first non-None result, see :ref:`firstresult` """ @hookspec(firstresult=True) def pytest_pyfunc_call(pyfuncitem): - """ call underlying test function. """ + """ call underlying test function. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_generate_tests(metafunc): """ generate (multiple) parametrized calls to a test function.""" @@ -163,7 +180,8 @@ def pytest_make_parametrize_id(config, val, argname): """Return a user-friendly string representation of the given ``val`` that will be used by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. The parameter name is available as ``argname``, if required. - """ + + Stops at first non-None result, see :ref:`firstresult` """ # ------------------------------------------------------------------------- # generic runtest related hooks @@ -172,7 +190,9 @@ def pytest_make_parametrize_id(config, val, argname): @hookspec(firstresult=True) def pytest_runtestloop(session): """ called for performing the main runtest loop - (after collection finished). """ + (after collection finished). + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_itemstart(item, node): """ (deprecated, use pytest_runtest_logstart). """ @@ -190,7 +210,9 @@ def pytest_runtest_protocol(item, nextitem): :py:func:`pytest_runtest_teardown`. :return boolean: True if no further hook implementations should be invoked. - """ + + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_runtest_logstart(nodeid, location): """ signal the start of running a single test item. """ @@ -215,7 +237,8 @@ def pytest_runtest_makereport(item, call): """ return a :py:class:`_pytest.runner.TestReport` object for the given :py:class:`pytest.Item <_pytest.main.Item>` and :py:class:`_pytest.runner.CallInfo`. - """ + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_runtest_logreport(report): """ process a test setup/call/teardown report relating to @@ -227,7 +250,9 @@ def pytest_runtest_logreport(report): @hookspec(firstresult=True) def pytest_fixture_setup(fixturedef, request): - """ performs fixture setup execution. """ + """ performs fixture setup execution. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_fixture_post_finalizer(fixturedef): """ called after fixture teardown, but before the cache is cleared so @@ -277,7 +302,9 @@ def pytest_report_header(config, startdir): @hookspec(firstresult=True) def pytest_report_teststatus(report): - """ return result-category, shortletter and verbose word for reporting.""" + """ return result-category, shortletter and verbose word for reporting. + + Stops at first non-None result, see :ref:`firstresult` """ def pytest_terminal_summary(terminalreporter, exitstatus): """ add additional section in terminal summary reporting. """ @@ -295,7 +322,9 @@ def pytest_logwarning(message, code, nodeid, fslocation): @hookspec(firstresult=True) def pytest_doctest_prepare_content(content): - """ return processed content for a given doctest""" + """ return processed content for a given doctest + + Stops at first non-None result, see :ref:`firstresult` """ # ------------------------------------------------------------------------- # error handling and internal debugging hooks diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 38bbe887d..9f5190c3e 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -255,11 +255,11 @@ if ``myapp.testsupport.myplugin`` also declares ``pytest_plugins``, the contents of the variable will also be loaded as plugins, and so on. This mechanism makes it easy to share fixtures within applications or even -external applications without the need to create external plugins using +external applications without the need to create external plugins using the ``setuptools``'s entry point technique. Plugins imported by ``pytest_plugins`` will also automatically be marked -for assertion rewriting (see :func:`pytest.register_assert_rewrite`). +for assertion rewriting (see :func:`pytest.register_assert_rewrite`). However for this to have any effect the module must not be imported already; if it was already imported at the time the ``pytest_plugins`` statement is processed, a warning will result and @@ -357,6 +357,8 @@ allowed to raise exceptions. Doing so will break the pytest run. +.. _firstresult: + firstresult: stop at first non-None result ------------------------------------------- From 4a992bafdbd822e5b32ad249a9509b0f52cbd4ed Mon Sep 17 00:00:00 2001 From: Max Moroz Date: Mon, 12 Jun 2017 19:55:30 -0700 Subject: [PATCH 09/21] Changelog --- changelog/2493.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2493.doc diff --git a/changelog/2493.doc b/changelog/2493.doc new file mode 100644 index 000000000..619963041 --- /dev/null +++ b/changelog/2493.doc @@ -0,0 +1 @@ +Explicitly state for which hooks the calls stop after the first non-None result. \ No newline at end of file From 218af423256ad8b9224086fa6f23f98a2cbbda37 Mon Sep 17 00:00:00 2001 From: Raphael Pierzina Date: Tue, 13 Jun 2017 14:58:07 +0200 Subject: [PATCH 10/21] Update copyright date in LICENSE and README.rst --- LICENSE | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 9e27bd784..629df45ac 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2004-2016 Holger Krekel and others +Copyright (c) 2004-2017 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 3a0abc5c1..cf2304ed8 100644 --- a/README.rst +++ b/README.rst @@ -102,7 +102,7 @@ Consult the `Changelog `__ page License ------- -Copyright Holger Krekel and others, 2004-2016. +Copyright Holger Krekel and others, 2004-2017. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. From 9970dea8c1cce98a19da539e36d6f13744565c5a Mon Sep 17 00:00:00 2001 From: Raphael Pierzina Date: Tue, 13 Jun 2017 14:59:11 +0200 Subject: [PATCH 11/21] Update copyright date in doc pages --- doc/en/index.rst | 4 ++-- doc/en/license.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index cb901b8d5..77e019d70 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -83,8 +83,8 @@ Consult the :ref:`Changelog ` page for fixes and enhancements of each License ------- -Copyright Holger Krekel and others, 2004-2016. +Copyright Holger Krekel and others, 2004-2017. Distributed under the terms of the `MIT`_ license, pytest is free and open source software. -.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE \ No newline at end of file +.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE diff --git a/doc/en/license.rst b/doc/en/license.rst index 3fc1dad52..b8c0dce1b 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-2016 Holger Krekel and others + Copyright (c) 2004-2017 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 0e73724e5854b301ee2aa37650b2f78eca423e56 Mon Sep 17 00:00:00 2001 From: Raphael Pierzina Date: Tue, 13 Jun 2017 15:15:52 +0200 Subject: [PATCH 12/21] Add changelog/2499.trivial --- changelog/2499.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2499.trivial diff --git a/changelog/2499.trivial b/changelog/2499.trivial new file mode 100644 index 000000000..1b4341725 --- /dev/null +++ b/changelog/2499.trivial @@ -0,0 +1 @@ +Update copyright dates in LICENSE, README.rst and in the documentation. From b09d60c60a476a09c67f8378135a3ef0386cc2c8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 13 Jun 2017 19:34:05 -0300 Subject: [PATCH 13/21] Fix decode error in Python 2 for doctests in docstrings Fix #2434 --- _pytest/doctest.py | 9 ++++----- changelog/2434.bugfix | 1 + testing/test_doctest.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 changelog/2434.bugfix diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 46b49d212..fde6dd71d 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -177,7 +177,6 @@ class DoctestTextfile(pytest.Module): name = self.fspath.basename globs = {'__name__': '__main__'} - optionflags = get_optionflags(self) runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, checker=_get_checker()) @@ -218,9 +217,6 @@ class DoctestModule(pytest.Module): runner = doctest.DebugRunner(verbose=0, optionflags=optionflags, checker=_get_checker()) - encoding = self.config.getini("doctest_encoding") - _fix_spoof_python2(runner, encoding) - for test in finder.find(module, module.__name__): if test.examples: # skip empty doctests yield DoctestItem(test.name, self, runner, test) @@ -332,7 +328,10 @@ def _get_report_choice(key): def _fix_spoof_python2(runner, encoding): """ - Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. + Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This + should patch only doctests for text files because they don't have a way to declare their + encoding. Doctests in docstrings from Python modules don't have the same problem given that + Python already decoded the strings. This fixes the problem related in issue #2434. """ diff --git a/changelog/2434.bugfix b/changelog/2434.bugfix new file mode 100644 index 000000000..172a992c4 --- /dev/null +++ b/changelog/2434.bugfix @@ -0,0 +1 @@ +Fix decode error in Python 2 for doctests in docstrings. diff --git a/testing/test_doctest.py b/testing/test_doctest.py index e22976c75..26f9c8469 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -527,6 +527,25 @@ class TestDoctests(object): '*1 failed*', ]) + def test_unicode_doctest_module(self, testdir): + """ + Test case for issue 2434: DecodeError on Python 2 when doctest docstring + contains non-ascii characters. + """ + p = testdir.makepyfile(test_unicode_doctest_module=""" + # -*- encoding: utf-8 -*- + from __future__ import unicode_literals + + def fix_bad_unicode(text): + ''' + >>> print(fix_bad_unicode('único')) + único + ''' + return "único" + """) + result = testdir.runpytest(p, '--doctest-modules') + result.stdout.fnmatch_lines(['* 1 passed *']) + class TestLiterals(object): From 4e4ebbef5a30c28085e0d4a01676f21ed15263f4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 13 Jun 2017 19:54:14 -0300 Subject: [PATCH 14/21] Improve test to ensure the expected function is re-raised --- _pytest/fixtures.py | 5 ++++- changelog/2440.bugfix | 2 +- testing/python/fixture.py | 19 +++++++++++++------ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 5799b00e6..64d21b9f6 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -742,7 +742,10 @@ class FixtureDef: except: exceptions.append(sys.exc_info()) if exceptions: - py.builtin._reraise(*exceptions[0]) + e = exceptions[0] + del exceptions # ensure we don't keep all frames alive because of the traceback + py.builtin._reraise(*e) + finally: ihook = self._fixturemanager.session.ihook ihook.pytest_fixture_post_finalizer(fixturedef=self) diff --git a/changelog/2440.bugfix b/changelog/2440.bugfix index 6f64d1dee..7f1f7d504 100644 --- a/changelog/2440.bugfix +++ b/changelog/2440.bugfix @@ -1 +1 @@ -Exceptions in a SubRequest's finish() block are suppressed until all finalizers are called, with the initial exception reraised. \ No newline at end of file +Exceptions raised during teardown by finalizers are now suppressed until all finalizers are called, with the initial exception reraised. diff --git a/testing/python/fixture.py b/testing/python/fixture.py index ca2e078bd..1e58a5550 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -658,11 +658,15 @@ class TestRequestBasic(object): ]) def test_request_subrequest_addfinalizer_exceptions(self, testdir): + """ + Ensure exceptions raised during teardown by a finalizer are suppressed + until all finalizers are called, re-raising the first exception (#2440) + """ testdir.makepyfile(""" import pytest l = [] - def _excepts(): - raise Exception('Error') + def _excepts(where): + raise Exception('Error in %s fixture' % where) @pytest.fixture def subrequest(request): return request @@ -670,18 +674,21 @@ class TestRequestBasic(object): def something(subrequest): subrequest.addfinalizer(lambda: l.append(1)) subrequest.addfinalizer(lambda: l.append(2)) - subrequest.addfinalizer(_excepts) + subrequest.addfinalizer(lambda: _excepts('something')) @pytest.fixture def excepts(subrequest): - subrequest.addfinalizer(_excepts) + subrequest.addfinalizer(lambda: _excepts('excepts')) subrequest.addfinalizer(lambda: l.append(3)) def test_first(something, excepts): pass def test_second(): assert l == [3, 2, 1] """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=2, failed=1) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*Exception: Error in excepts fixture', + '* 2 passed, 1 error in *', + ]) def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") From 97367cf773ec185d9dca6bb87873c35901ce4bc0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 13 Jun 2017 23:12:33 -0300 Subject: [PATCH 15/21] Remove obsolete comment from rewrite.py This was made obsolete by 021e843427c6f5e79ee4a5b47ef3015599292822 --- _pytest/assertion/rewrite.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 79943cc53..6ec54d7e7 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -162,10 +162,6 @@ class AssertionRewritingHook(object): # modules not passed explicitly on the command line are only # rewritten if they match the naming convention for test files for pat in self.fnpats: - # use fnmatch instead of fn_pypath.fnmatch because the - # latter might trigger an import to fnmatch.fnmatch - # internally, which would cause this method to be - # called recursively if fn_pypath.fnmatch(pat): state.trace("matched test file %r" % (fn,)) return True From 731c35fcabef1e145e8049e1bf565a7c18ca328d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 14 Jun 2017 07:43:21 -0400 Subject: [PATCH 16/21] Remove MANIFEST.in and related lint check Because setuptools_scm already includes all version-controlled files in an sdist, we don't need to maintain a MANIFEST.in file and anymore See pytest-dev/pytest-xdist#161 --- MANIFEST.in | 39 --------------------------------------- scripts/check-manifest.py | 20 -------------------- tox.ini | 2 -- 3 files changed, 61 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 scripts/check-manifest.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index abf57fece..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,39 +0,0 @@ -include CHANGELOG.rst -include LICENSE -include AUTHORS -include pyproject.toml - -include README.rst -include CONTRIBUTING.rst -include HOWTORELEASE.rst - -include tox.ini -include setup.py - -recursive-include changelog * -recursive-include scripts *.py -recursive-include scripts *.bat - -include .coveragerc - -recursive-include bench *.py -recursive-include extra *.py - -graft testing -graft doc -prune doc/en/_build -graft tasks - -exclude _pytest/impl - -graft _pytest/vendored_packages - -recursive-exclude * *.pyc *.pyo -recursive-exclude testing/.hypothesis * -recursive-exclude testing/freeze/~ * -recursive-exclude testing/freeze/build * -recursive-exclude testing/freeze/dist * - -exclude appveyor.yml -exclude .travis.yml -prune .github diff --git a/scripts/check-manifest.py b/scripts/check-manifest.py deleted file mode 100644 index de1357685..000000000 --- a/scripts/check-manifest.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Script used by tox.ini to check the manifest file if we are under version control, or skip the -check altogether if not. - -"check-manifest" will needs a vcs to work, which is not available when testing the package -instead of the source code (with ``devpi test`` for example). -""" - -from __future__ import print_function - -import os -import subprocess -import sys -from check_manifest import main - -if os.path.isdir('.git'): - sys.exit(main()) -else: - print('No .git directory found, skipping checking the manifest file') - sys.exit(0) diff --git a/tox.ini b/tox.ini index b73deca7d..d8b27300f 100644 --- a/tox.ini +++ b/tox.ini @@ -57,9 +57,7 @@ deps = # pygments required by rst-lint pygments restructuredtext_lint - check-manifest commands = - {envpython} scripts/check-manifest.py flake8 pytest.py _pytest testing {envpython} scripts/check-rst.py From afe847ecdc96060874e591d1c713b27ee09dee62 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 20 Jun 2017 23:43:34 -0300 Subject: [PATCH 17/21] fixture docs: highlight difference between yield and addfinalizer methods Fix #2508 --- doc/en/fixture.rst | 80 +++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index f760c423e..f305cb2c2 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -123,20 +123,13 @@ with a list of available function arguments. but is not anymore advertised as the primary means of declaring fixture functions. -"Funcargs" a prime example of dependency injection +Fixtures: a prime example of dependency injection --------------------------------------------------- -When injecting fixtures to test functions, pytest-2.0 introduced the -term "funcargs" or "funcarg mechanism" which continues to be present -also in docs today. It now refers to the specific case of injecting -fixture values as arguments to test functions. With pytest-2.3 there are -more possibilities to use fixtures but "funcargs" remain as the main way -as they allow to directly state the dependencies of a test function. - -As the following examples show in more detail, funcargs allow test -functions to easily receive and work against specific pre-initialized -application objects without having to care about import/setup/cleanup -details. It's a prime example of `dependency injection`_ where fixture +Fixtures allow test functions to easily receive and work +against specific pre-initialized application objects without having +to care about import/setup/cleanup details. +It's a prime example of `dependency injection`_ where fixture functions take the role of the *injector* and test functions are the *consumers* of fixture objects. @@ -296,6 +289,9 @@ The ``smtp`` connection will be closed after the test finished execution because the ``smtp`` object automatically closes when the ``with`` statement ends. +Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the +*teardown* code (after the ``yield``) will not be called. + .. note:: Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one @@ -303,29 +299,49 @@ the ``with`` statement ends. fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed and considered deprecated. -.. note:: - As historical note, another way to write teardown code is - by accepting a ``request`` object into your fixture function and can call its - ``request.addfinalizer`` one or multiple times:: - # content of conftest.py +An alternative option for executing *teardown* code is to +make use of the ``addfinalizer`` method of the `request-context`_ object to register +finalization functions. - import smtplib - import pytest +Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup: - @pytest.fixture(scope="module") - def smtp(request): - smtp = smtplib.SMTP("smtp.gmail.com") - def fin(): - print ("teardown smtp") - smtp.close() - request.addfinalizer(fin) - return smtp # provide the fixture value +.. code-block:: python - The ``fin`` function will execute when the last test in the module has finished execution. + # content of conftest.py + import smtplib + import pytest + + @pytest.fixture(scope="module") + def smtp(request): + smtp = smtplib.SMTP("smtp.gmail.com") + def fin(): + print ("teardown smtp") + smtp.close() + request.addfinalizer(fin) + return smtp # provide the fixture value + +Both ``yield`` and ``addfinalizer`` methods work similar by calling their code after the test +ends, but ``addfinalizer`` has two key differences over ``yield``: + +1. It is possible to register multiple finalizer functions. + +2. Finalizers will always be called regardless if the fixture *setup* code raises an exception. + This is handy to properly close all resources created by a fixture even if one of them + fails to be created/acquired:: + + @pytest.fixture + def equipments(request): + r = [] + for port in ('C1', 'C3', 'C28'): + equip = connect(port) + request.addfinalizer(equip.disconnect) + r.append(equip) + return r + + In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still + be properly closed. - This method is still fully supported, but ``yield`` is recommended from 2.10 onward because - it is considered simpler and better describes the natural code flow. .. _`request-context`: @@ -782,8 +798,8 @@ Autouse fixtures (xUnit setup on steroids) .. regendoc:wipe Occasionally, you may want to have fixtures get invoked automatically -without a `usefixtures`_ or `funcargs`_ reference. As a practical -example, suppose we have a database fixture which has a +without declaring a function argument explicitly or a `usefixtures`_ decorator. +As a practical example, suppose we have a database fixture which has a begin/rollback/commit architecture and we want to automatically surround each test method by a transaction and a rollback. Here is a dummy self-contained implementation of this idea:: From f2ba8d70b975933ecfb33ba978d5c6ceab4d366b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 21 Jun 2017 09:06:52 -0300 Subject: [PATCH 18/21] Fix typo and add suggestion from review --- doc/en/fixture.rst | 88 ++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index f305cb2c2..c5bf67053 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -73,20 +73,20 @@ marked ``smtp`` fixture function. Running the test looks like this:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 items - + test_smtpsimple.py F - + ======= FAILURES ======== _______ test_ehlo ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_smtpsimple.py:11: AssertionError ======= 1 failed in 0.12 seconds ======== @@ -169,7 +169,7 @@ function (in or below the directory where ``conftest.py`` is located):: response, msg = smtp.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg - assert 0 # for demo purposes + assert 0 # for demo purposes def test_noop(smtp): response, msg = smtp.noop() @@ -184,32 +184,32 @@ inspect what is going on and can now run the tests:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - + test_module.py FF - + ======= FAILURES ======== _______ test_ehlo ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 # for demo purposes E assert 0 - + test_module.py:6: AssertionError _______ test_noop ________ - + smtp = - + def test_noop(smtp): response, msg = smtp.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError ======= 2 failed in 0.12 seconds ======== @@ -260,7 +260,7 @@ Let's execute it:: $ pytest -s -q --tb=no FFteardown smtp - + 2 failed in 0.12 seconds We see that the ``smtp`` instance is finalized after the two @@ -290,7 +290,7 @@ because the ``smtp`` object automatically closes when the ``with`` statement ends. Note that if an exception happens during the *setup* code (before the ``yield`` keyword), the -*teardown* code (after the ``yield``) will not be called. +*teardown* code (after the ``yield``) will not be called. .. note:: @@ -302,7 +302,7 @@ Note that if an exception happens during the *setup* code (before the ``yield`` An alternative option for executing *teardown* code is to make use of the ``addfinalizer`` method of the `request-context`_ object to register -finalization functions. +finalization functions. Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup: @@ -321,7 +321,8 @@ Here's the ``smtp`` fixture changed to use ``addfinalizer`` for cleanup: request.addfinalizer(fin) return smtp # provide the fixture value -Both ``yield`` and ``addfinalizer`` methods work similar by calling their code after the test + +Both ``yield`` and ``addfinalizer`` methods work similarly by calling their code after the test ends, but ``addfinalizer`` has two key differences over ``yield``: 1. It is possible to register multiple finalizer functions. @@ -340,7 +341,8 @@ ends, but ``addfinalizer`` has two key differences over ``yield``: return r In the example above, if ``"C28"`` fails with an exception, ``"C1"`` and ``"C3"`` will still - be properly closed. + be properly closed. Of course, if an exception happens before the finalize function is + registered then it will not be executed. .. _`request-context`: @@ -371,7 +373,7 @@ again, nothing much has changed:: $ pytest -s -q --tb=no FFfinalizing (smtp.gmail.com) - + 2 failed in 0.12 seconds Let's quickly create another test module that actually sets the @@ -439,51 +441,51 @@ So let's just do another run:: FFFF ======= FAILURES ======== _______ test_ehlo[smtp.gmail.com] ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 assert b"smtp.gmail.com" in msg > assert 0 # for demo purposes E assert 0 - + test_module.py:6: AssertionError _______ test_noop[smtp.gmail.com] ________ - + smtp = - + def test_noop(smtp): response, msg = smtp.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError _______ test_ehlo[mail.python.org] ________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 > assert b"smtp.gmail.com" in msg E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nSIZE 51200000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8' - + test_module.py:5: AssertionError -------------------------- Captured stdout setup --------------------------- finalizing _______ test_noop[mail.python.org] ________ - + smtp = - + def test_noop(smtp): response, msg = smtp.noop() assert response == 250 > assert 0 # for demo purposes E assert 0 - + test_module.py:11: AssertionError ------------------------- Captured stdout teardown ------------------------- finalizing @@ -555,7 +557,7 @@ Running the above tests results in the following test IDs being used:: - + ======= no tests ran in 0.12 seconds ======== .. _`interdependent fixtures`: @@ -594,10 +596,10 @@ Here we declare an ``app`` fixture which receives the previously defined cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items - + test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED test_appsetup.py::test_smtp_exists[mail.python.org] PASSED - + ======= 2 passed in 0.12 seconds ======== Due to the parametrization of ``smtp`` the test will run twice with two @@ -663,26 +665,26 @@ Let's run the tests in verbose mode and with looking at the print-output:: cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 8 items - + test_module.py::test_0[1] SETUP otherarg 1 RUN test0 with otherarg 1 PASSED TEARDOWN otherarg 1 - + test_module.py::test_0[2] SETUP otherarg 2 RUN test0 with otherarg 2 PASSED TEARDOWN otherarg 2 - + test_module.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 PASSED test_module.py::test_2[1-mod1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 PASSED TEARDOWN otherarg 1 - + test_module.py::test_2[2-mod1] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 PASSED TEARDOWN otherarg 2 - + test_module.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 @@ -690,13 +692,13 @@ Let's run the tests in verbose mode and with looking at the print-output:: test_module.py::test_2[1-mod2] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 PASSED TEARDOWN otherarg 1 - + test_module.py::test_2[2-mod2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 PASSED TEARDOWN otherarg 2 TEARDOWN modarg mod2 - - + + ======= 8 passed in 0.12 seconds ======== You can see that the parametrized module-scoped ``modarg`` resource caused an From ff8dbd0ad8d50cf1ff9c07e0c6e04fe26d58cc9a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 22 Jun 2017 08:54:39 -0300 Subject: [PATCH 19/21] Add tracebackhide to function call form of deprecated_call --- _pytest/recwarn.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 36b22e940..9cc404a49 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -45,6 +45,7 @@ def deprecated_call(func=None, *args, **kwargs): if not func: return _DeprecatedCallContext() else: + __tracebackhide__ = True with _DeprecatedCallContext(): return func(*args, **kwargs) @@ -71,7 +72,7 @@ class _DeprecatedCallContext(object): def __exit__(self, exc_type, exc_val, exc_tb): warnings.warn_explicit = self._old_warn_explicit warnings.warn = self._old_warn - + if exc_type is None: deprecation_categories = (DeprecationWarning, PendingDeprecationWarning) if not any(issubclass(c, deprecation_categories) for c in self._captured_categories): From 6de19ab7baf24561450302b071075296a5c1d9eb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 23 Jun 2017 12:30:29 -0300 Subject: [PATCH 20/21] Show "trivial" category in CHANGELOG I think it might sense to display in the CHANGELOG internal or trivial changes because they might trip users between releases. For example, a note about an internal refactoring (like moving a class between modules) is useful for a user that has been using the internal API. Of course we are not breaking anything because it was an internal API, but no reason not to save time for users who did use it. --- changelog/_template.rst | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog/_template.rst b/changelog/_template.rst index 4f1d46d80..66c850ffd 100644 --- a/changelog/_template.rst +++ b/changelog/_template.rst @@ -6,7 +6,7 @@ {% endif %} {% if sections[section] %} -{% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %} +{% for category, val in definitions.items() if category in sections[section] %} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} diff --git a/pyproject.toml b/pyproject.toml index 138fd4ce6..88571e208 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,5 +31,5 @@ template = "changelog/_template.rst" [[tool.towncrier.type]] directory = "trivial" - name = "Trivial Changes" - showcontent = false + name = "Trivial/Internal Changes" + showcontent = true From b3bf7fc4960ec1aec54f09de8713bfba777b1683 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 23 Jun 2017 20:49:12 +0200 Subject: [PATCH 21/21] add tasks for updating vendored libs --- changelog/2474.trivial | 1 + tasks/__init__.py | 8 ++++++-- tasks/vendoring.py | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 changelog/2474.trivial create mode 100644 tasks/vendoring.py diff --git a/changelog/2474.trivial b/changelog/2474.trivial new file mode 100644 index 000000000..9ea3fb651 --- /dev/null +++ b/changelog/2474.trivial @@ -0,0 +1 @@ +Create invoke tasks for updating the vendored packages. \ No newline at end of file diff --git a/tasks/__init__.py b/tasks/__init__.py index 9551ff059..992f4a4ad 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -4,6 +4,10 @@ Invoke tasks to help with pytest development and release process. import invoke -from . import generate +from . import generate, vendoring -ns = invoke.Collection(generate) + +ns = invoke.Collection( + generate, + vendoring +) diff --git a/tasks/vendoring.py b/tasks/vendoring.py new file mode 100644 index 000000000..867f2946b --- /dev/null +++ b/tasks/vendoring.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, print_function +import py +import invoke + +VENDOR_TARGET = py.path.local("_pytest/vendored_packages") +GOOD_FILES = 'README.md', '__init__.py' + +@invoke.task() +def remove_libs(ctx): + print("removing vendored libs") + for path in VENDOR_TARGET.listdir(): + if path.basename not in GOOD_FILES: + print(" ", path) + path.remove() + +@invoke.task(pre=[remove_libs]) +def update_libs(ctx): + print("installing libs") + ctx.run("pip install -t {target} pluggy".format(target=VENDOR_TARGET)) + ctx.run("git add {target}".format(target=VENDOR_TARGET)) + print("Please commit to finish the update after running the tests:") + print() + print(' git commit -am "Updated vendored libs"')