Merge remote-tracking branch 'upstream/master' into merge-upstream

This commit is contained in:
Bruno Oliveira 2018-02-17 09:38:06 -02:00
commit 69d608aec3
25 changed files with 201 additions and 38 deletions

View File

@ -1,15 +1,14 @@
Thanks for submitting a PR, your contribution is really appreciated! 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 - [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](/changelog/README.rst) for details.
* name it `$issue_id.$type` for example (588.bugfix) - [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes.
* if you don't have an issue_id change it to the pr id after creating the pr - [ ] Target the `features` branch for new features and removals/deprecations.
* ensure type is one of `removal`, `feature`, `bugfix`, `vendor`, `doc` or `trivial` - [ ] Include documentation when adding new features.
* Make sure to use full sentences with correct case and punctuation, for example: "Fix issue with non-ascii contents in doctest text files." - [ ] Include new tests or update existing tests when applicable.
- [ ] 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
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;

View File

@ -39,9 +39,6 @@ matrix:
python: '3.5' python: '3.5'
- env: TOXENV=py37 - env: TOXENV=py37
python: 'nightly' python: 'nightly'
allow_failures:
- env: TOXENV=py37
python: 'nightly'
script: tox --recreate script: tox --recreate

View File

@ -29,6 +29,7 @@ Benjamin Peterson
Bernard Pratz Bernard Pratz
Bob Ippolito Bob Ippolito
Brian Dorsey Brian Dorsey
Brian Maissy
Brian Okken Brian Okken
Brianna Laugher Brianna Laugher
Bruno Oliveira Bruno Oliveira

View File

@ -79,10 +79,11 @@ def num_mock_patch_args(function):
patchings = getattr(function, "patchings", None) patchings = getattr(function, "patchings", None)
if not patchings: if not patchings:
return 0 return 0
mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None)) mock_modules = [sys.modules.get("mock"), sys.modules.get("unittest.mock")]
if mock is not None: if any(mock_modules):
sentinels = [m.DEFAULT for m in mock_modules if m is not None]
return len([p for p in patchings 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) return len(patchings)

View File

@ -2,6 +2,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import pdb import pdb
import sys import sys
from doctest import UnexpectedException
def pytest_addoption(parser): def pytest_addoption(parser):
@ -85,6 +86,17 @@ def _enter_pdb(node, excinfo, rep):
# for not completely clear reasons. # for not completely clear reasons.
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
tw.line() 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") tw.sep(">", "traceback")
rep.toterminal(tw) rep.toterminal(tw)
tw.sep(">", "entering PDB") tw.sep(">", "entering PDB")
@ -95,10 +107,9 @@ def _enter_pdb(node, excinfo, rep):
def _postmortem_traceback(excinfo): 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): 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] return excinfo.value.exc_info[2]
else: else:
return excinfo._excinfo[2] return excinfo._excinfo[2]

View File

@ -166,7 +166,7 @@ def reorder_items(items):
items_by_argkey = {} items_by_argkey = {}
for scopenum in range(0, scopenum_function): for scopenum in range(0, scopenum_function):
argkeys_cache[scopenum] = d = {} argkeys_cache[scopenum] = d = {}
items_by_argkey[scopenum] = item_d = defaultdict(list) items_by_argkey[scopenum] = item_d = defaultdict(deque)
for item in items: for item in items:
keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum)) keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
if keys: if keys:
@ -174,12 +174,19 @@ def reorder_items(items):
for key in keys: for key in keys:
item_d[key].append(item) item_d[key].append(item)
items = OrderedDict.fromkeys(items) 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: if scopenum >= scopenum_function or len(items) < 3:
return items return items
ignore = set()
items_deque = deque(items) items_deque = deque(items)
items_done = OrderedDict() items_done = OrderedDict()
scoped_items_by_argkey = items_by_argkey[scopenum] 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: else:
slicing_argkey, _ = argkeys.popitem() slicing_argkey, _ = argkeys.popitem()
# we don't have to remove relevant items from later in the deque because they'll just be ignored # 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]): matching_items = [i for i in scoped_items_by_argkey[slicing_argkey] if i in items]
if i in items: for i in reversed(matching_items):
items_deque.appendleft(i) fix_cache_order(i, argkeys_cache, items_by_argkey)
items_deque.appendleft(i)
break break
if no_argkey_group: if no_argkey_group:
no_argkey_group = reorder_items_atscope( 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: for item in no_argkey_group:
items_done[item] = None items_done[item] = None
ignore.add(slicing_argkey) ignore.add(slicing_argkey)

View File

@ -504,7 +504,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
if not self._test_outcome_written: if not self._test_outcome_written:
self._test_outcome_written = True self._test_outcome_written = True
self.stream.write('\n') 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.stream.section('live log ' + self._when, sep='-', bold=True)
self._section_name_shown = True self._section_name_shown = True
logging.StreamHandler.emit(self, record) logging.StreamHandler.emit(self, record)

View File

@ -75,7 +75,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
return cls(argval, marks=newmarks, id=None) return cls(argval, marks=newmarks, id=None)
@classmethod @classmethod
def _for_parameterize(cls, argnames, argvalues, function, config): def _for_parametrize(cls, argnames, argvalues, function, config):
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()] argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1 force_tuple = len(argnames) == 1

View File

@ -785,7 +785,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
from _pytest.fixtures import scope2index from _pytest.fixtures import scope2index
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
from py.io import saferepr from py.io import saferepr
argnames, parameters = ParameterSet._for_parameterize(
argnames, parameters = ParameterSet._for_parametrize(
argnames, argvalues, self.function, self.config) argnames, argvalues, self.function, self.config)
del argvalues del argvalues

View File

@ -453,6 +453,10 @@ def raises(expected_exception, *args, **kwargs):
Assert that a code block/function call raises ``expected_exception`` Assert that a code block/function call raises ``expected_exception``
and raise a failure exception otherwise. 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). This helper produces a ``ExceptionInfo()`` object (see below).
You may use this function as a context manager:: You may use this function as a context manager::

View File

@ -0,0 +1 @@
Move import of ``doctest.UnexpectedException`` to top-level to avoid possible errors when using ``--pdb``.

1
changelog/3052.bugfix Normal file
View File

@ -0,0 +1 @@
Added printing of captured stdout/stderr before entering pdb, and improved a test which was giving false negatives about output capturing.

View File

@ -0,0 +1 @@
Fix ordering of tests using parametrized fixtures which can lead to fixtures being created more than necessary.

View File

@ -0,0 +1 @@
Rename ``ParameterSet._for_parameterize()`` to ``_for_parametrize()`` in order to comply with the naming convention.

1
changelog/3184.bugfix Normal file
View File

@ -0,0 +1 @@
Fix bug where logging happening at hooks outside of "test run" hooks would cause an internal error.

1
changelog/3202.doc.rst Normal file
View File

@ -0,0 +1 @@
Add Sphinx parameter docs for ``match`` and ``message`` args to ``pytest.raises``.

View File

@ -0,0 +1 @@
Detect arguments injected by ``unittest.mock.patch`` decorator correctly when pypi ``mock.patch`` is installed and imported.

View File

@ -0,0 +1 @@
Skip failing pdb/doctest test on mac.

30
changelog/README.rst Normal file
View File

@ -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 ``<ISSUE>.<TYPE>.rst``, where
``<ISSUE>`` is an issue number, and ``<TYPE>`` 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.

View File

@ -462,19 +462,24 @@ Here is an example definition of a hook wrapper::
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
# do whatever you want before the next hook executes do_something_before_next_hook_executes()
outcome = yield outcome = yield
# outcome.excinfo may be None or a (cls, val, tb) tuple # outcome.excinfo may be None or a (cls, val, tb) tuple
res = outcome.get_result() # will raise if outcome was exception 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 Note that hook wrappers don't return results themselves, they merely
perform tracing or other side effects around the actual hook implementations. 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 If the result of the underlying hook is a mutable object, they may modify
that result but it's probably better to avoid it. that result but it's probably better to avoid it.
For more information, consult the `pluggy documentation <http://pluggy.readthedocs.io/en/latest/#wrappers>`_.
Hook function ordering / call example Hook function ordering / call example
------------------------------------- -------------------------------------

View File

@ -16,7 +16,7 @@ classifiers = [
'Topic :: Utilities', 'Topic :: Utilities',
] + [ ] + [
('Programming Language :: Python :: %s' % x) ('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: with open('README.rst') as fd:

View File

@ -2168,6 +2168,47 @@ class TestFixtureMarker(object):
test_mod1.py::test_func1[m2] PASSED 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): def test_class_ordering(self, testdir):
testdir.makeini(""" testdir.makeini("""
[pytest] [pytest]

View File

@ -147,6 +147,28 @@ class TestMockDecoration(object):
reprec = testdir.inline_run() reprec = testdir.inline_run()
reprec.assertoutcome(passed=1) 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): def test_mock(self, testdir):
pytest.importorskip("mock", "1.0.1") pytest.importorskip("mock", "1.0.1")
testdir.makepyfile(""" testdir.makepyfile("""

View File

@ -141,19 +141,50 @@ class TestPDB(object):
child.sendeof() child.sendeof()
self.flush(child) self.flush(child)
def test_pdb_interaction_capture(self, testdir): def test_pdb_print_captured_stdout(self, testdir):
p1 = testdir.makepyfile(""" p1 = testdir.makepyfile("""
def test_1(): def test_1():
print("getrekt") print("get\\x20rekt")
assert False assert False
""") """)
child = testdir.spawn_pytest("--pdb %s" % p1) child = testdir.spawn_pytest("--pdb %s" % p1)
child.expect("getrekt") child.expect("captured stdout")
child.expect("get rekt")
child.expect("(Pdb)") child.expect("(Pdb)")
child.sendeof() child.sendeof()
rest = child.read().decode("utf8") rest = child.read().decode("utf8")
assert "1 failed" in rest 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) self.flush(child)
def test_pdb_interaction_exception(self, testdir): def test_pdb_interaction_exception(self, testdir):
@ -267,6 +298,10 @@ class TestPDB(object):
child.read() child.read()
self.flush(child) 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): def test_pdb_interaction_doctest(self, testdir):
p1 = testdir.makepyfile(""" p1 = testdir.makepyfile("""
import pytest import pytest

View File

@ -206,13 +206,13 @@ class TestWarns(object):
with pytest.warns(RuntimeWarning): with pytest.warns(RuntimeWarning):
warnings.warn("user", UserWarning) warnings.warn("user", UserWarning)
excinfo.match(r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) was emitted. " 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.raises(pytest.fail.Exception) as excinfo:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
warnings.warn("runtime", RuntimeWarning) warnings.warn("runtime", RuntimeWarning)
excinfo.match(r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) was emitted. " 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.raises(pytest.fail.Exception) as excinfo:
with pytest.warns(UserWarning): with pytest.warns(UserWarning):