diff --git a/changelog/4400.bugfix.rst b/changelog/4400.bugfix.rst new file mode 100644 index 000000000..eb0df7eca --- /dev/null +++ b/changelog/4400.bugfix.rst @@ -0,0 +1 @@ +Rearrange warning handling for the yield test errors so the opt-out in 4.0.x correctly works. diff --git a/changelog/4405.bugfix.rst b/changelog/4405.bugfix.rst new file mode 100644 index 000000000..ac05091c1 --- /dev/null +++ b/changelog/4405.bugfix.rst @@ -0,0 +1 @@ +Fix collection of testpaths with ``--pyargs``. diff --git a/changelog/4412.bugfix.rst b/changelog/4412.bugfix.rst new file mode 100644 index 000000000..7a28b6108 --- /dev/null +++ b/changelog/4412.bugfix.rst @@ -0,0 +1 @@ +Fix assertion rewriting involving ``Starred`` + side-effects. diff --git a/setup.py b/setup.py index ac5f3ffe8..696fc4028 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,16 @@ def main(): use_scm_version={"write_to": "src/_pytest/_version.py"}, setup_requires=["setuptools-scm", "setuptools>=40.0"], package_dir={"": "src"}, + # fmt: off + extras_require={ + "testing": [ + "hypothesis>=3.56", + "nose", + "requests", + "mock;python_version=='2.7'", + ], + }, + # fmt: on install_requires=INSTALL_REQUIRES, ) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ecb24ff7c..7b9aa5006 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -946,7 +946,8 @@ class AssertionRewriter(ast.NodeVisitor): def visit_Starred(self, starred): # From Python 3.5, a Starred node can appear in a function call res, expl = self.visit(starred.value) - return starred, "*" + expl + new_starred = ast.Starred(res, starred.ctx) + return new_starred, "*" + expl def visit_Call_legacy(self, call): """ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ba11c8055..1de824eb0 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -852,10 +852,7 @@ class Config(object): ) if not args: if self.invocation_dir == self.rootdir: - args = [ - str(self.invocation_dir.join(x, abs=True)) - for x in self.getini("testpaths") - ] + args = self.getini("testpaths") if not args: args = [str(self.invocation_dir)] self.args = args diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 24ddf4e9e..c59628948 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -518,6 +518,9 @@ class Testdir(object): def __repr__(self): return "" % (self.tmpdir,) + def __str__(self): + return str(self.tmpdir) + def finalize(self): """Clean up global state artifacts. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d360e2c8f..8912ca060 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -741,16 +741,20 @@ class FunctionMixin(PyobjMixin): class Generator(FunctionMixin, PyCollector): def collect(self): + # test generators are seen as collectors but they also # invoke setup/teardown on popular request # (induced by the common "test_*" naming shared with normal tests) from _pytest import deprecated + self.warn(deprecated.YIELD_TESTS) + self.session._setupstate.prepare(self) # see FunctionMixin.setup and test_setupstate_is_preserved_134 self._preservedparent = self.parent.obj values = [] seen = {} + _Function = self._getcustomclass("Function") for i, x in enumerate(self.obj()): name, call, args = self.getcallargs(x) if not callable(call): @@ -764,11 +768,7 @@ class Generator(FunctionMixin, PyCollector): "%r generated tests with non-unique name %r" % (self, name) ) seen[name] = True - with warnings.catch_warnings(): - # ignore our own deprecation warning - function_class = self.Function - values.append(function_class(name, self, args=args, callobj=call)) - self.warn(deprecated.YIELD_TESTS) + values.append(_Function(name, self, args=args, callobj=call)) return values def getcallargs(self, obj): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index acbabb68b..a02433cd6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -413,6 +413,19 @@ class TestAssertionRewrite(object): ) testdir.runpytest().assert_outcomes(passed=1) + @pytest.mark.skipif("sys.version_info < (3,5)") + def test_starred_with_side_effect(self, testdir): + """See #4412""" + testdir.makepyfile( + """\ + def test(): + f = lambda x: x + x = iter([1, 2, 3]) + assert 2 * next(x) == f(*[next(x)]) + """ + ) + testdir.runpytest().assert_outcomes(passed=1) + def test_call(self): def g(a=42, *args, **kwargs): return False diff --git a/testing/test_collection.py b/testing/test_collection.py index 9385f5461..fae23025e 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1086,6 +1086,28 @@ def test_collect_with_chdir_during_import(testdir): result.stdout.fnmatch_lines(["collected 1 item"]) +def test_collect_pyargs_with_testpaths(testdir, monkeypatch): + testmod = testdir.mkdir("testmod") + # NOTE: __init__.py is not collected since it does not match python_files. + testmod.ensure("__init__.py").write("def test_func(): pass") + testmod.ensure("test_file.py").write("def test_func(): pass") + + root = testdir.mkdir("root") + root.ensure("pytest.ini").write( + textwrap.dedent( + """ + [pytest] + addopts = --pyargs + testpaths = testmod + """ + ) + ) + monkeypatch.setenv("PYTHONPATH", str(testdir.tmpdir)) + with root.as_cwd(): + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines(["*1 passed in*"]) + + @pytest.mark.skipif( not hasattr(py.path.local, "mksymlinkto"), reason="symlink not available on this platform", diff --git a/testing/test_session.py b/testing/test_session.py index e2c3701da..0dc98a703 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -4,6 +4,7 @@ from __future__ import print_function import pytest from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG class SessionTests(object): @@ -77,7 +78,8 @@ class SessionTests(object): """ def test_1(): yield None - """ + """, + SHOW_PYTEST_WARNINGS_ARG, ) failures = reprec.getfailedcollections() out = failures[0].longrepr.reprcrash.message diff --git a/tox.ini b/tox.ini index 8fe0124f3..92cdfc85a 100644 --- a/tox.ini +++ b/tox.ini @@ -28,11 +28,8 @@ setenv = coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess coverage: COVERAGE_FILE={toxinidir}/.coverage coverage: COVERAGE_PROCESS_START={toxinidir}/.coveragerc +extras = testing deps = - hypothesis>=3.56 - nose - {py27,pypy}: mock - requests {env:_PYTEST_TOX_EXTRA_DEP:} [testenv:py27-subprocess] @@ -51,22 +48,18 @@ deps = pre-commit>=1.11.0 commands = pre-commit run --all-files --show-diff-on-failure [testenv:py27-xdist] +extras = testing deps = pytest-xdist>=1.13 - {py27,pypy}: mock - nose - hypothesis>=3.56 {env:_PYTEST_TOX_EXTRA_DEP:} commands = {env:_PYTEST_TOX_COVERAGE_RUN:} pytest -n auto {posargs} [testenv:py37-xdist] # NOTE: copied from above due to https://github.com/tox-dev/tox/issues/706. +extras = testing deps = pytest-xdist>=1.13 - {py27,pypy}: mock - nose - hypothesis>=3.56 {env:_PYTEST_TOX_EXTRA_DEP:} commands = {[testenv:py27-xdist]commands} @@ -76,7 +69,7 @@ deps = pexpect {env:_PYTEST_TOX_EXTRA_DEP:} commands = - {env:_PYTEST_TOX_COVERAGE_RUN:} pytest testing/test_pdb.py testing/test_terminal.py testing/test_unittest.py {posargs} + {env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:testing/test_pdb.py testing/test_terminal.py testing/test_unittest.py} [testenv:py37-pexpect] platform = {[testenv:py27-pexpect]platform} @@ -84,10 +77,9 @@ deps = {[testenv:py27-pexpect]deps} commands = {[testenv:py27-pexpect]commands} [testenv:py27-nobyte] +extras = testing deps = pytest-xdist>=1.13 - hypothesis>=3.56 - py27: mock {env:_PYTEST_TOX_EXTRA_DEP:} distribute = true setenv = @@ -219,6 +211,8 @@ filterwarnings = ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning # pytest's own futurewarnings ignore::pytest.PytestExperimentalApiWarning + # Do not cause SyntaxError for invalid escape sequences in py37. + default:invalid escape sequence:DeprecationWarning pytester_example_dir = testing/example_scripts [flake8] max-line-length = 120