diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 690426d68..23a9f8c56 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,14 @@ Thanks for submitting a PR, your contribution is really appreciated! -Here's a quick checklist that should be present in PRs: +Here's a quick checklist that should be present in PRs (you can delete this text from the final description, this is +just a guideline): -- [ ] Add a new news fragment into the changelog folder - * name it `$issue_id.$type` for example (588.bugfix) - * if you don't have an issue_id change it to the pr id after creating the pr - * ensure type is one of `removal`, `feature`, `bugfix`, `vendor`, `doc` or `trivial` - * Make sure to use full sentences with correct case and punctuation, for example: "Fix issue with non-ascii contents in doctest text files." -- [ ] Target: for `bugfix`, `vendor`, `doc` or `trivial` fixes, target `master`; for removals or features target `features`; -- [ ] Make sure to include reasonable tests for your change if necessary +- [ ] Create a new changelog file in the `changelog` folder, with a name like `..rst`. See [changelog/README.rst](/changelog/README.rst) for details. +- [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes. +- [ ] Target the `features` branch for new features and removals/deprecations. +- [ ] Include documentation when adding new features. +- [ ] Include new tests or update existing tests when applicable. -Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please: +Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please: -- [ ] Add yourself to `AUTHORS`, in alphabetical order; +- [ ] Add yourself to `AUTHORS` in alphabetical order; diff --git a/.travis.yml b/.travis.yml index 938391cde..40fe3e8ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,9 +39,6 @@ matrix: python: '3.5' - env: TOXENV=py37 python: 'nightly' - allow_failures: - - env: TOXENV=py37 - python: 'nightly' script: tox --recreate diff --git a/AUTHORS b/AUTHORS index a4ce2e8e6..cda6511a0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -29,6 +29,7 @@ Benjamin Peterson Bernard Pratz Bob Ippolito Brian Dorsey +Brian Maissy Brian Okken Brianna Laugher Bruno Oliveira diff --git a/_pytest/compat.py b/_pytest/compat.py index 7560fbec3..92df65656 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -79,10 +79,11 @@ def num_mock_patch_args(function): patchings = getattr(function, "patchings", None) if not patchings: return 0 - mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None)) - if mock is not None: + mock_modules = [sys.modules.get("mock"), sys.modules.get("unittest.mock")] + if any(mock_modules): + sentinels = [m.DEFAULT for m in mock_modules if m is not None] return len([p for p in patchings - if not p.attribute_name and p.new is mock.DEFAULT]) + if not p.attribute_name and p.new in sentinels]) return len(patchings) diff --git a/_pytest/debugging.py b/_pytest/debugging.py index 23d94e688..43472f23b 100644 --- a/_pytest/debugging.py +++ b/_pytest/debugging.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function import pdb import sys +from doctest import UnexpectedException def pytest_addoption(parser): @@ -85,6 +86,17 @@ def _enter_pdb(node, excinfo, rep): # for not completely clear reasons. tw = node.config.pluginmanager.getplugin("terminalreporter")._tw tw.line() + + captured_stdout = rep.capstdout + if len(captured_stdout) > 0: + tw.sep(">", "captured stdout") + tw.line(captured_stdout) + + captured_stderr = rep.capstderr + if len(captured_stderr) > 0: + tw.sep(">", "captured stderr") + tw.line(captured_stderr) + tw.sep(">", "traceback") rep.toterminal(tw) tw.sep(">", "entering PDB") @@ -95,10 +107,9 @@ def _enter_pdb(node, excinfo, rep): def _postmortem_traceback(excinfo): - # A doctest.UnexpectedException is not useful for post_mortem. - # Use the underlying exception instead: - from doctest import UnexpectedException if isinstance(excinfo.value, UnexpectedException): + # A doctest.UnexpectedException is not useful for post_mortem. + # Use the underlying exception instead: return excinfo.value.exc_info[2] else: return excinfo._excinfo[2] diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 2bc6f108b..a8445767c 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -166,7 +166,7 @@ def reorder_items(items): items_by_argkey = {} for scopenum in range(0, scopenum_function): argkeys_cache[scopenum] = d = {} - items_by_argkey[scopenum] = item_d = defaultdict(list) + items_by_argkey[scopenum] = item_d = defaultdict(deque) for item in items: keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum)) if keys: @@ -174,12 +174,19 @@ def reorder_items(items): for key in keys: item_d[key].append(item) items = OrderedDict.fromkeys(items) - return list(reorder_items_atscope(items, set(), argkeys_cache, items_by_argkey, 0)) + return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0)) -def reorder_items_atscope(items, ignore, argkeys_cache, items_by_argkey, scopenum): +def fix_cache_order(item, argkeys_cache, items_by_argkey): + for scopenum in range(0, scopenum_function): + for key in argkeys_cache[scopenum].get(item, []): + items_by_argkey[scopenum][key].appendleft(item) + + +def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum): if scopenum >= scopenum_function or len(items) < 3: return items + ignore = set() items_deque = deque(items) items_done = OrderedDict() scoped_items_by_argkey = items_by_argkey[scopenum] @@ -197,13 +204,14 @@ def reorder_items_atscope(items, ignore, argkeys_cache, items_by_argkey, scopenu else: slicing_argkey, _ = argkeys.popitem() # we don't have to remove relevant items from later in the deque because they'll just be ignored - for i in reversed(scoped_items_by_argkey[slicing_argkey]): - if i in items: - items_deque.appendleft(i) + matching_items = [i for i in scoped_items_by_argkey[slicing_argkey] if i in items] + for i in reversed(matching_items): + fix_cache_order(i, argkeys_cache, items_by_argkey) + items_deque.appendleft(i) break if no_argkey_group: no_argkey_group = reorder_items_atscope( - no_argkey_group, set(), argkeys_cache, items_by_argkey, scopenum + 1) + no_argkey_group, argkeys_cache, items_by_argkey, scopenum + 1) for item in no_argkey_group: items_done[item] = None ignore.add(slicing_argkey) diff --git a/_pytest/logging.py b/_pytest/logging.py index b50347518..d3ac81e65 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -504,7 +504,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): if not self._test_outcome_written: self._test_outcome_written = True self.stream.write('\n') - if not self._section_name_shown: + if not self._section_name_shown and self._when: self.stream.section('live log ' + self._when, sep='-', bold=True) self._section_name_shown = True logging.StreamHandler.emit(self, record) diff --git a/_pytest/mark.py b/_pytest/mark.py index b49f0fc64..3cac9dc91 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -75,7 +75,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): return cls(argval, marks=newmarks, id=None) @classmethod - def _for_parameterize(cls, argnames, argvalues, function, config): + def _for_parametrize(cls, argnames, argvalues, function, config): 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/_pytest/python.py b/_pytest/python.py index 2a84677ec..fb7bac8b8 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -785,7 +785,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): from _pytest.fixtures import scope2index from _pytest.mark import ParameterSet from py.io import saferepr - argnames, parameters = ParameterSet._for_parameterize( + + argnames, parameters = ParameterSet._for_parametrize( argnames, argvalues, self.function, self.config) del argvalues diff --git a/_pytest/python_api.py b/_pytest/python_api.py index 81960295b..e6f002849 100644 --- a/_pytest/python_api.py +++ b/_pytest/python_api.py @@ -453,6 +453,10 @@ def raises(expected_exception, *args, **kwargs): Assert that a code block/function call raises ``expected_exception`` and raise a failure exception otherwise. + :arg message: if specified, provides a custom failure message if the + exception is not raised + :arg match: if specified, asserts that the exception matches a text or regex + This helper produces a ``ExceptionInfo()`` object (see below). You may use this function as a context manager:: diff --git a/changelog/1810.bugfix.rst b/changelog/1810.bugfix.rst new file mode 100644 index 000000000..c91ed47d0 --- /dev/null +++ b/changelog/1810.bugfix.rst @@ -0,0 +1 @@ +Move import of ``doctest.UnexpectedException`` to top-level to avoid possible errors when using ``--pdb``. diff --git a/changelog/3052.bugfix b/changelog/3052.bugfix new file mode 100644 index 000000000..ea8c362a4 --- /dev/null +++ b/changelog/3052.bugfix @@ -0,0 +1 @@ +Added printing of captured stdout/stderr before entering pdb, and improved a test which was giving false negatives about output capturing. diff --git a/changelog/3161.bugfix.rst b/changelog/3161.bugfix.rst new file mode 100644 index 000000000..73872be67 --- /dev/null +++ b/changelog/3161.bugfix.rst @@ -0,0 +1 @@ +Fix ordering of tests using parametrized fixtures which can lead to fixtures being created more than necessary. diff --git a/changelog/3166.trivial.rst b/changelog/3166.trivial.rst new file mode 100644 index 000000000..ce92840cb --- /dev/null +++ b/changelog/3166.trivial.rst @@ -0,0 +1 @@ +Rename ``ParameterSet._for_parameterize()`` to ``_for_parametrize()`` in order to comply with the naming convention. diff --git a/changelog/3184.bugfix b/changelog/3184.bugfix new file mode 100644 index 000000000..875358776 --- /dev/null +++ b/changelog/3184.bugfix @@ -0,0 +1 @@ +Fix bug where logging happening at hooks outside of "test run" hooks would cause an internal error. diff --git a/changelog/3202.doc.rst b/changelog/3202.doc.rst new file mode 100644 index 000000000..a6f99fbf6 --- /dev/null +++ b/changelog/3202.doc.rst @@ -0,0 +1 @@ +Add Sphinx parameter docs for ``match`` and ``message`` args to ``pytest.raises``. diff --git a/changelog/3206.bugfix.rst b/changelog/3206.bugfix.rst new file mode 100644 index 000000000..1e2305fa2 --- /dev/null +++ b/changelog/3206.bugfix.rst @@ -0,0 +1 @@ +Detect arguments injected by ``unittest.mock.patch`` decorator correctly when pypi ``mock.patch`` is installed and imported. diff --git a/changelog/985.trivial.rst b/changelog/985.trivial.rst new file mode 100644 index 000000000..8554f2b65 --- /dev/null +++ b/changelog/985.trivial.rst @@ -0,0 +1 @@ +Skip failing pdb/doctest test on mac. diff --git a/changelog/README.rst b/changelog/README.rst new file mode 100644 index 000000000..f00720de9 --- /dev/null +++ b/changelog/README.rst @@ -0,0 +1,30 @@ +This directory contains "newsfragments" which are short that contain a small **ReST**-formatted +text that will be added to the next ``CHANGELOG``. + +The ``CHANGELOG`` will be read by users, so this description should be aimed to pytest users +instead of describing internal changes which are only relevant to the developers. + +Make sure to use full sentences with correct case and punctuation, for example: *Fix issue with non-ascii contents in doctest text files.* + +Each file should be named like ``..rst``, where +```` is an issue number, and ```` is one of: + +* ``feature``: new user facing features, like new command-line options and new behavior. +* ``bugfix``: fixes a reported bug. +* ``doc``: documentation improvement, like rewording an entire session or adding missing docs. +* ``removal``: feature deprecation or removal. +* ``vendor``: changes in packages vendored in pytest. +* ``trivial``: fixing a small typo or internal change that might be noteworthy. + +So for example: ``123.feature.rst``, ``456.bugfix.rst``. + +If your PR fixes an issue, use that number here. If there is no issue, +then after you submit the PR and get the PR number you can add a +changelog using that instead. + +If you are not sure what issue type to use, don't hesitate to ask in your PR. + +Note that the ``towncrier`` tool will automatically +reflow your text, so it will work best if you stick to a single paragraph, but multiple sentences and links are OK +and encouraged. You can install ``towncrier`` and then run ``towncrier --draft`` +if you want to get a preview of how your change will look in the final release notes. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 7787f8d32..f5a8ea41e 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -462,19 +462,24 @@ Here is an example definition of a hook wrapper:: @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): - # do whatever you want before the next hook executes + do_something_before_next_hook_executes() outcome = yield # outcome.excinfo may be None or a (cls, val, tb) tuple res = outcome.get_result() # will raise if outcome was exception - # postprocess result + + post_process_result(res) + + outcome.force_result(new_res) # to override the return value to the plugin system Note that hook wrappers don't return results themselves, they merely perform tracing or other side effects around the actual hook implementations. If the result of the underlying hook is a mutable object, they may modify that result but it's probably better to avoid it. +For more information, consult the `pluggy documentation `_. + Hook function ordering / call example ------------------------------------- diff --git a/setup.py b/setup.py index e08be845e..30234d2cc 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ classifiers = [ 'Topic :: Utilities', ] + [ ('Programming Language :: Python :: %s' % x) - for x in '2 2.7 3 3.4 3.5 3.6'.split() + for x in '2 2.7 3 3.4 3.5 3.6 3.7'.split() ] with open('README.rst') as fd: diff --git a/testing/python/fixture.py b/testing/python/fixture.py index d22389e71..6bcb1ab00 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2168,6 +2168,47 @@ class TestFixtureMarker(object): test_mod1.py::test_func1[m2] PASSED """) + def test_dynamic_parametrized_ordering(self, testdir): + testdir.makeini(""" + [pytest] + console_output_style=classic + """) + testdir.makeconftest(""" + import pytest + + def pytest_configure(config): + class DynamicFixturePlugin(object): + @pytest.fixture(scope='session', params=['flavor1', 'flavor2']) + def flavor(self, request): + return request.param + config.pluginmanager.register(DynamicFixturePlugin(), 'flavor-fixture') + + @pytest.fixture(scope='session', params=['vxlan', 'vlan']) + def encap(request): + return request.param + + @pytest.fixture(scope='session', autouse='True') + def reprovision(request, flavor, encap): + pass + """) + testdir.makepyfile(""" + def test(reprovision): + pass + def test2(reprovision): + pass + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines(""" + test_dynamic_parametrized_ordering.py::test[flavor1-vxlan] PASSED + test_dynamic_parametrized_ordering.py::test2[flavor1-vxlan] PASSED + test_dynamic_parametrized_ordering.py::test[flavor2-vxlan] PASSED + test_dynamic_parametrized_ordering.py::test2[flavor2-vxlan] PASSED + test_dynamic_parametrized_ordering.py::test[flavor2-vlan] PASSED + test_dynamic_parametrized_ordering.py::test2[flavor2-vlan] PASSED + test_dynamic_parametrized_ordering.py::test[flavor1-vlan] PASSED + test_dynamic_parametrized_ordering.py::test2[flavor1-vlan] PASSED + """) + def test_class_ordering(self, testdir): testdir.makeini(""" [pytest] diff --git a/testing/python/integration.py b/testing/python/integration.py index 6ea29fa98..aade04fa9 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -147,6 +147,28 @@ class TestMockDecoration(object): reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + def test_unittest_mock_and_pypi_mock(self, testdir): + pytest.importorskip("unittest.mock") + pytest.importorskip("mock", "1.0.1") + testdir.makepyfile(""" + import mock + import unittest.mock + class TestBoth(object): + @unittest.mock.patch("os.path.abspath") + def test_hello(self, abspath): + import os + os.path.abspath("hello") + abspath.assert_any_call("hello") + + @mock.patch("os.path.abspath") + def test_hello_mock(self, abspath): + import os + os.path.abspath("hello") + abspath.assert_any_call("hello") + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2) + def test_mock(self, testdir): pytest.importorskip("mock", "1.0.1") testdir.makepyfile(""" diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 8618473bb..d882c2cf6 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -141,19 +141,50 @@ class TestPDB(object): child.sendeof() self.flush(child) - def test_pdb_interaction_capture(self, testdir): + def test_pdb_print_captured_stdout(self, testdir): p1 = testdir.makepyfile(""" def test_1(): - print("getrekt") + print("get\\x20rekt") assert False """) child = testdir.spawn_pytest("--pdb %s" % p1) - child.expect("getrekt") + child.expect("captured stdout") + child.expect("get rekt") child.expect("(Pdb)") child.sendeof() rest = child.read().decode("utf8") assert "1 failed" in rest - assert "getrekt" not in rest + assert "get rekt" not in rest + self.flush(child) + + def test_pdb_print_captured_stderr(self, testdir): + p1 = testdir.makepyfile(""" + def test_1(): + import sys + sys.stderr.write("get\\x20rekt") + assert False + """) + child = testdir.spawn_pytest("--pdb %s" % p1) + child.expect("captured stderr") + child.expect("get rekt") + child.expect("(Pdb)") + child.sendeof() + rest = child.read().decode("utf8") + assert "1 failed" in rest + assert "get rekt" not in rest + self.flush(child) + + def test_pdb_dont_print_empty_captured_stdout_and_stderr(self, testdir): + p1 = testdir.makepyfile(""" + def test_1(): + assert False + """) + child = testdir.spawn_pytest("--pdb %s" % p1) + child.expect("(Pdb)") + output = child.before.decode("utf8") + child.sendeof() + assert "captured stdout" not in output + assert "captured stderr" not in output self.flush(child) def test_pdb_interaction_exception(self, testdir): @@ -267,6 +298,10 @@ class TestPDB(object): child.read() self.flush(child) + # For some reason the interaction between doctest's and pytest's output + # capturing mechanisms are messing up the stdout on mac. (See #985). + # Should be solvable, but skipping until we have a chance to investigate. + @pytest.mark.xfail("sys.platform == 'darwin'", reason='See issue #985', run=False) def test_pdb_interaction_doctest(self, testdir): p1 = testdir.makepyfile(""" import pytest diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 0c80c169e..f1cf542e9 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -206,13 +206,13 @@ class TestWarns(object): with pytest.warns(RuntimeWarning): warnings.warn("user", UserWarning) excinfo.match(r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) was emitted. " - r"The list of emitted warnings is: \[UserWarning\('user',\)\].") + r"The list of emitted warnings is: \[UserWarning\('user',?\)\].") with pytest.raises(pytest.fail.Exception) as excinfo: with pytest.warns(UserWarning): warnings.warn("runtime", RuntimeWarning) excinfo.match(r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) was emitted. " - r"The list of emitted warnings is: \[RuntimeWarning\('runtime',\)\].") + r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)\].") with pytest.raises(pytest.fail.Exception) as excinfo: with pytest.warns(UserWarning):