diff --git a/AUTHORS b/AUTHORS index 9b4b8b8cc..d99261478 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Ahn Ki-Wook Alan Velasco Alexander Johnson Alexei Kozlenok +Allan Feldman Anatoly Bubenkoff Anders Hovmöller Andras Tim @@ -94,6 +95,7 @@ Hui Wang (coldnight) Ian Bicking Ian Lesperance Ionuț Turturică +Iwan Briquemont Jaap Broekhuizen Jan Balster Janne Vanhala @@ -179,6 +181,7 @@ Raphael Pierzina Raquel Alegre Ravi Chandra Roberto Polli +Roland Puntaier Romain Dorgueil Roman Bolshakov Ronny Pfannschmidt @@ -223,6 +226,5 @@ Wim Glenn Wouter van Ackooy Xuan Luong Xuecong Liao +Zac Hatfield-Dodds Zoltán Máté -Roland Puntaier -Allan Feldman diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d68a160ab..26bc28af1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,58 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 3.8.2 (2018-10-02) +========================= + +Deprecations and Removals +------------------------- + +- `#4036 `_: The ``item`` parameter of ``pytest_warning_captured`` hook is now documented as deprecated. We realized only after + the ``3.8`` release that this parameter is incompatible with ``pytest-xdist``. + + Our policy is to not deprecate features during bugfix releases, but in this case we believe it makes sense as we are + only documenting it as deprecated, without issuing warnings which might potentially break test suites. This will get + the word out that hook implementers should not use this parameter at all. + + In a future release ``item`` will always be ``None`` and will emit a proper warning when a hook implementation + makes use of it. + + + +Bug Fixes +--------- + +- `#3539 `_: Fix reload on assertion rewritten modules. + + +- `#4034 `_: The ``.user_properties`` attribute of ``TestReport`` objects is a list + of (name, value) tuples, but could sometimes be instantiated as a tuple + of tuples. It is now always a list. + + +- `#4039 `_: No longer issue warnings about using ``pytest_plugins`` in non-top-level directories when using ``--pyargs``: the + current ``--pyargs`` mechanism is not reliable and might give false negatives. + + +- `#4040 `_: Exclude empty reports for passed tests when ``-rP`` option is used. + + +- `#4051 `_: Improve error message when an invalid Python expression is passed to the ``-m`` option. + + +- `#4056 `_: ``MonkeyPatch.setenv`` and ``MonkeyPatch.delenv`` issue a warning if the environment variable name is not ``str`` on Python 2. + + In Python 2, adding ``unicode`` keys to ``os.environ`` causes problems with ``subprocess`` (and possible other modules), + making this a subtle bug specially susceptible when used with ``from __future__ import unicode_literals``. + + + +Improved Documentation +---------------------- + +- `#3928 `_: Add possible values for fixture scope to docs. + + pytest 3.8.1 (2018-09-22) ========================= diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index cdedd3ca7..e6c712120 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-3.8.2 release-3.8.1 release-3.8.0 release-3.7.4 diff --git a/doc/en/announce/release-3.8.2.rst b/doc/en/announce/release-3.8.2.rst new file mode 100644 index 000000000..124c33aa4 --- /dev/null +++ b/doc/en/announce/release-3.8.2.rst @@ -0,0 +1,28 @@ +pytest-3.8.2 +======================================= + +pytest 3.8.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/latest/changelog.html. + +Thanks to all who contributed to this release, among them: + +* Ankit Goel +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* Denis Otkidach +* Harry Percival +* Jeffrey Rackauckas +* Jose Carlos Menezes +* Ronny Pfannschmidt +* Zac-HD +* iwanb + + +Happy testing, +The pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index e0e9b9305..d3deeb503 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -264,8 +264,12 @@ Advanced assertion introspection Reporting details about a failing assertion is achieved by rewriting assert statements before they are run. Rewritten assert statements put introspection information into the assertion failure message. ``pytest`` only rewrites test -modules directly discovered by its test collection process, so asserts in -supporting modules which are not themselves test modules will not be rewritten. +modules directly discovered by its test collection process, so **asserts in +supporting modules which are not themselves test modules will not be rewritten**. + +You can manually enable assertion rewriting for an imported module by calling +`register_assert_rewrite `_ +before you import it (a good place to do that is in ``conftest.py``). .. note:: diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 3c35fbf4e..268786d58 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -83,14 +83,24 @@ message please contact the authors so they can change the code. Those methods were part of the internal pytest warnings system, but since ``3.8`` pytest is using the builtin warning system for its own warnings, so those two functions are now deprecated. -``Config.warn`` should be replaced by calls to the standard ``warnings.warn``. +``Config.warn`` should be replaced by calls to the standard ``warnings.warn``, example: + +.. code-block:: python + + config.warn("C1", "some warning") + +Becomes: + +.. code-block:: python + + warnings.warn(pytest.PytestWarning("some warning")) ``Node.warn`` now supports two signatures: -* ``node.warn(PytestWarning("some message"))``: is now the recommended way to call this function. +* ``node.warn(PytestWarning("some message"))``: is now the **recommended** way to call this function. The warning instance must be a PytestWarning or subclass. -* ``node.warn("CI", "some message")``: this code/message form is now deprecated and should be converted to the warning instance form above. +* ``node.warn("CI", "some message")``: this code/message form is now **deprecated** and should be converted to the warning instance form above. ``pytest_namespace`` diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index cb6368a64..a2cb8a676 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -31,7 +31,7 @@ You can then restrict a test run to only run tests marked with ``webtest``:: $ pytest -v -m webtest =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 3 deselected @@ -44,7 +44,7 @@ Or the inverse, running all tests except the webtest ones:: $ pytest -v -m "not webtest" =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 1 deselected @@ -64,7 +64,7 @@ tests based on their module, class, method, or function name:: $ pytest -v test_server.py::TestClass::test_method =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -77,7 +77,7 @@ You can also select on the class:: $ pytest -v test_server.py::TestClass =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item @@ -90,7 +90,7 @@ Or select multiple nodes:: $ pytest -v test_server.py::TestClass test_server.py::test_send_http =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -128,7 +128,7 @@ select tests based on their names:: $ pytest -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 3 deselected @@ -141,7 +141,7 @@ And you can also run all tests except the ones that match the keyword:: $ pytest -k "not send_http" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 1 deselected @@ -156,7 +156,7 @@ Or to select "http" and "quick" tests:: $ pytest -k "http or quick" -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items / 2 deselected diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index bda15065a..a266b5bfe 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -59,7 +59,7 @@ consulted when reporting in ``verbose`` mode:: nonpython $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collecting ... collected 2 items diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 5af269bba..7dca5510a 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -411,11 +411,10 @@ is to be run with different sets of arguments for its three arguments: Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize):: . $ pytest -rs -q multipython.py - ...ssssssssssssssssssssssss [100%] + ...sss...sssssssss...sss... [100%] ========================= short test summary info ========================== - SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.4' not found - SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found - 3 passed, 24 skipped in 0.12 seconds + SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.4' not found + 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index c6e6c428e..8c3975c60 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -357,7 +357,7 @@ which will add info only when run with "--v":: $ pytest -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache info1: did you know that ... did you? diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 8ea13c7f4..d90850d09 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -171,6 +171,7 @@ to cause the decorated ``smtp_connection`` fixture function to only be invoked once per test *module* (the default is to invoke once per test *function*). Multiple test functions in a test module will thus each receive the same ``smtp_connection`` fixture instance, thus saving time. +Possible values for ``scope`` are: ``function``, ``class``, ``module``, ``package`` or ``session``. The next example puts the fixture function into a separate ``conftest.py`` file so that tests from multiple test modules in the directory can @@ -726,7 +727,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``:: $ pytest test_fixture_marks.py -v =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 3 items @@ -769,7 +770,7 @@ Here we declare an ``app`` fixture which receives the previously defined $ pytest -v test_appsetup.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items @@ -838,7 +839,7 @@ Let's run the tests in verbose mode and with looking at the print-output:: $ pytest -v -s test_module.py =========================== test session starts ============================ - platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.6 + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: .pytest_cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 8 items diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 4da786101..e5521bba5 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -255,7 +255,7 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours: - When ``breakpoint()`` is called and ``PYTHONBREAKPOINT`` is set to the default value, pytest will use the custom internal PDB trace UI instead of the system default ``Pdb``. - When tests are complete, the system will default back to the system ``Pdb`` trace UI. - - If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on ``bothbreakpoint()`` and failed tests/unhandled exceptions. + - If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on both ``breakpoint()`` and failed tests/unhandled exceptions. - If ``--pdbcls`` is used, the custom class debugger will be executed when a test fails (as expected within existing behaviour), but also when ``breakpoint()`` is called from within a test, the custom class debugger will be instantiated. .. _durations: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index be8c6dc4d..7a11c4ec1 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -269,17 +269,17 @@ class AssertionRewritingHook(object): ) def load_module(self, name): - # If there is an existing module object named 'fullname' in - # sys.modules, the loader must use that existing module. (Otherwise, - # the reload() builtin will not work correctly.) - if name in sys.modules: - return sys.modules[name] - co, pyc = self.modules.pop(name) - # I wish I could just call imp.load_compiled here, but __file__ has to - # be set properly. In Python 3.2+, this all would be handled correctly - # by load_compiled. - mod = sys.modules[name] = imp.new_module(name) + if name in sys.modules: + # If there is an existing module object named 'fullname' in + # sys.modules, the loader must use that existing module. (Otherwise, + # the reload() builtin will not work correctly.) + mod = sys.modules[name] + else: + # I wish I could just call imp.load_compiled here, but __file__ has to + # be set properly. In Python 3.2+, this all would be handled correctly + # by load_compiled. + mod = sys.modules[name] = imp.new_module(name) try: mod.__file__ = co.co_filename # Normally, this attribute is 3.2+. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index bc45d65a9..0f8d2d5f3 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -351,6 +351,7 @@ class PytestPluginManager(PluginManager): else None ) self._noconftest = namespace.noconftest + self._using_pyargs = namespace.pyargs testpaths = namespace.file_or_dir foundanchor = False for path in testpaths: @@ -416,7 +417,11 @@ class PytestPluginManager(PluginManager): _ensure_removed_sysmodule(conftestpath.purebasename) try: mod = conftestpath.pyimport() - if hasattr(mod, "pytest_plugins") and self._configured: + if ( + hasattr(mod, "pytest_plugins") + and self._configured + and not self._using_pyargs + ): from _pytest.deprecated import ( PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST ) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1a9326149..533806964 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -562,6 +562,9 @@ def pytest_warning_captured(warning_message, when, item): * ``"runtest"``: during test execution. :param pytest.Item|None item: + **DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None`` + in a future release. + The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. """ diff --git a/src/_pytest/mark/legacy.py b/src/_pytest/mark/legacy.py index ab016a035..0d0d852ce 100644 --- a/src/_pytest/mark/legacy.py +++ b/src/_pytest/mark/legacy.py @@ -66,7 +66,10 @@ python_keywords_allowed_list = ["or", "and", "not"] def matchmark(colitem, markexpr): """Tries to match on any marker names, attached to the given colitem.""" - return eval(markexpr, {}, MarkMapping.from_item(colitem)) + try: + return eval(markexpr, {}, MarkMapping.from_item(colitem)) + except SyntaxError as e: + raise SyntaxError(str(e) + "\nMarker expression must be valid Python!") def matchkeyword(colitem, keywordexpr): diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 67279003f..4bd6b607d 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,9 +4,12 @@ from __future__ import absolute_import, division, print_function import os import sys import re +import warnings from contextlib import contextmanager import six + +import pytest from _pytest.fixtures import fixture RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") @@ -209,13 +212,31 @@ class MonkeyPatch(object): self._setitem.append((dic, name, dic.get(name, notset))) del dic[name] + def _warn_if_env_name_is_not_str(self, name): + """On Python 2, warn if the given environment variable name is not a native str (#4056)""" + if six.PY2 and not isinstance(name, str): + warnings.warn( + pytest.PytestWarning( + "Environment variable name {!r} should be str".format(name) + ) + ) + def setenv(self, name, value, prepend=None): """ Set environment variable ``name`` to ``value``. If ``prepend`` is a character, read the current environment variable value and prepend the ``value`` adjoined with the ``prepend`` character.""" - value = str(value) + if not isinstance(value, str): + warnings.warn( + pytest.PytestWarning( + "Environment variable value {!r} should be str, converted to str implicitly".format( + value + ) + ) + ) + value = str(value) if prepend and name in os.environ: value = value + prepend + os.environ[name] + self._warn_if_env_name_is_not_str(name) self.setitem(os.environ, name, value) def delenv(self, name, raising=True): @@ -225,6 +246,7 @@ class MonkeyPatch(object): If ``raising`` is set to False, no exception will be raised if the environment variable is missing. """ + self._warn_if_env_name_is_not_str(name) self.delitem(os.environ, name, raising=raising) def syspath_prepend(self, path): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 0eee4c841..7a0586697 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -212,6 +212,8 @@ class WarningsChecker(WarningsRecorder): def __exit__(self, *exc_info): super(WarningsChecker, self).__exit__(*exc_info) + __tracebackhide__ = True + # only check if we're not currently handling an exception if all(a is None for a in exc_info): if self.expected_warning is not None: diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 33b906702..a57c9fa80 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -110,7 +110,7 @@ class TestReport(BaseReport): when, sections=(), duration=0, - user_properties=(), + user_properties=None, **extra ): #: normalized collection node id @@ -136,7 +136,7 @@ class TestReport(BaseReport): #: user properties is a list of tuples (name, value) that holds user #: defined properties of the test - self.user_properties = user_properties + self.user_properties = list(user_properties or []) #: list of pairs ``(str, str)`` of extra information which needs to #: marshallable. Used by pytest to add captured text diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 49a9a33fb..125eb97c4 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -745,9 +745,10 @@ class TerminalReporter(object): return self.write_sep("=", "PASSES") for rep in reports: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg) - self._outrep_summary(rep) + if rep.sections: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg) + self._outrep_summary(rep) def print_teardown_sections(self, rep): showcapture = self.config.option.showcapture diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 8a9585be2..332af27b5 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -577,7 +577,7 @@ class TestInvocationVariants(object): return what empty_package = testdir.mkpydir("empty_package") - monkeypatch.setenv("PYTHONPATH", join_pythonpath(empty_package)) + monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(empty_package))) # the path which is not a package raises a warning on pypy; # no idea why only pypy and not normal python warn about it here with warnings.catch_warnings(): @@ -586,7 +586,7 @@ class TestInvocationVariants(object): assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - monkeypatch.setenv("PYTHONPATH", join_pythonpath(testdir)) + monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(testdir))) result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True) assert result.ret != 0 result.stderr.fnmatch_lines(["*not*found*test_missing*"]) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 14f06acd0..66e880e05 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -129,7 +129,7 @@ def test_source_strip_multiline(): def test_syntaxerror_rerepresentation(): ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz") assert ex.value.lineno == 1 - assert ex.value.offset in (4, 7) # XXX pypy/jython versus cpython? + assert ex.value.offset in (4, 5, 7) # XXX pypy/jython versus cpython? assert ex.value.text.strip(), "x x" diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 49b21ccb1..649ebcd38 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -246,21 +246,12 @@ def test_pytest_catchlog_deprecated(testdir, plugin): def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - subdirectory = testdir.tmpdir.join("subdirectory") - subdirectory.mkdir() - # create the inner conftest with makeconftest and then move it to the subdirectory - testdir.makeconftest( - """ + testdir.makepyfile( + **{ + "subdirectory/conftest.py": """ pytest_plugins=['capture'] """ - ) - testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) - # make the top level conftest - testdir.makeconftest( - """ - import warnings - warnings.filterwarnings('always', category=DeprecationWarning) - """ + } ) testdir.makepyfile( """ @@ -268,7 +259,7 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): pass """ ) - res = testdir.runpytest_subprocess() + res = testdir.runpytest() assert res.ret == 0 msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] res.stdout.fnmatch_lines( @@ -278,6 +269,34 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): ) +@pytest.mark.parametrize("use_pyargs", [True, False]) +def test_pytest_plugins_in_non_top_level_conftest_deprecated_pyargs( + testdir, use_pyargs +): + """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" + from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST + + files = { + "src/pkg/__init__.py": "", + "src/pkg/conftest.py": "", + "src/pkg/test_root.py": "def test(): pass", + "src/pkg/sub/__init__.py": "", + "src/pkg/sub/conftest.py": "pytest_plugins=['capture']", + "src/pkg/sub/test_bar.py": "def test(): pass", + } + testdir.makepyfile(**files) + testdir.syspathinsert(testdir.tmpdir.join("src")) + + args = ("--pyargs", "pkg") if use_pyargs else () + res = testdir.runpytest(*args) + assert res.ret == 0 + msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] + if use_pyargs: + assert msg not in res.stdout.str() + else: + res.stdout.fnmatch_lines("*{msg}*".format(msg=msg)) + + def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_conftest( testdir ): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index a2cd8e81c..5153fc741 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1050,6 +1050,48 @@ class TestAssertionRewriteHookDetails(object): result = testdir.runpytest("-s") result.stdout.fnmatch_lines(["* 1 passed*"]) + def test_reload_reloads(self, testdir): + """Reloading a module after change picks up the change.""" + testdir.tmpdir.join("file.py").write( + textwrap.dedent( + """ + def reloaded(): + return False + + def rewrite_self(): + with open(__file__, 'w') as self: + self.write('def reloaded(): return True') + """ + ) + ) + testdir.tmpdir.join("pytest.ini").write( + textwrap.dedent( + """ + [pytest] + python_files = *.py + """ + ) + ) + + testdir.makepyfile( + test_fun=""" + import sys + try: + from imp import reload + except ImportError: + pass + + def test_loader(): + import file + assert not file.reloaded() + file.rewrite_self() + reload(file) + assert file.reloaded() + """ + ) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(["* 1 passed*"]) + def test_get_data_support(self, testdir): """Implement optional PEP302 api (#808). """ diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 5d73dc846..2444d8bc1 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -215,7 +215,7 @@ def test_cache_show(testdir): class TestLastFailed(object): def test_lastfailed_usecase(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) + monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") p = testdir.makepyfile( """ def test_1(): @@ -301,7 +301,7 @@ class TestLastFailed(object): assert "test_a.py" not in result.stdout.str() def test_lastfailed_difference_invocations(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) + monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") testdir.makepyfile( test_a="""\ def test_a1(): @@ -335,7 +335,7 @@ class TestLastFailed(object): result.stdout.fnmatch_lines(["*1 failed*1 desel*"]) def test_lastfailed_usecase_splice(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) + monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") testdir.makepyfile( """\ def test_1(): @@ -474,8 +474,8 @@ class TestLastFailed(object): ) def rlf(fail_import, fail_run): - monkeypatch.setenv("FAILIMPORT", fail_import) - monkeypatch.setenv("FAILTEST", fail_run) + monkeypatch.setenv("FAILIMPORT", str(fail_import)) + monkeypatch.setenv("FAILTEST", str(fail_run)) testdir.runpytest("-q") config = testdir.parseconfigure() @@ -519,8 +519,8 @@ class TestLastFailed(object): ) def rlf(fail_import, fail_run, args=()): - monkeypatch.setenv("FAILIMPORT", fail_import) - monkeypatch.setenv("FAILTEST", fail_run) + monkeypatch.setenv("FAILIMPORT", str(fail_import)) + monkeypatch.setenv("FAILTEST", str(fail_run)) result = testdir.runpytest("-q", "--lf", *args) config = testdir.parseconfigure() diff --git a/testing/test_conftest.py b/testing/test_conftest.py index f3b5bac38..07da5d5ee 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -30,6 +30,7 @@ def conftest_setinitial(conftest, args, confcutdir=None): self.file_or_dir = args self.confcutdir = str(confcutdir) self.noconftest = False + self.pyargs = False conftest._set_initial_conftests(Namespace()) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 3928548a8..2304c4a50 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -850,7 +850,7 @@ def test_logxml_path_expansion(tmpdir, monkeypatch): assert xml_tilde.logfile == home_tilde # this is here for when $HOME is not set correct - monkeypatch.setenv("HOME", tmpdir) + monkeypatch.setenv("HOME", str(tmpdir)) home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml")) xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None) diff --git a/testing/test_mark.py b/testing/test_mark.py index baf1d6f40..3e5c86624 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -799,6 +799,18 @@ class TestFunctional(object): deselected_tests = dlist[0].items assert len(deselected_tests) == 2 + def test_invalid_m_option(self, testdir): + testdir.makepyfile( + """ + def test_a(): + pass + """ + ) + result = testdir.runpytest("-m bogus/") + result.stdout.fnmatch_lines( + ["INTERNALERROR> Marker expression must be valid Python!"] + ) + def test_keywords_at_node_level(self, testdir): testdir.makepyfile( """ diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index adf10e4d0..853c0192f 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -3,6 +3,8 @@ import os import sys import textwrap +import six + import pytest from _pytest.monkeypatch import MonkeyPatch @@ -163,7 +165,8 @@ def test_delitem(): def test_setenv(): monkeypatch = MonkeyPatch() - monkeypatch.setenv("XYZ123", 2) + with pytest.warns(pytest.PytestWarning): + monkeypatch.setenv("XYZ123", 2) import os assert os.environ["XYZ123"] == "2" @@ -192,13 +195,49 @@ def test_delenv(): del os.environ[name] +class TestEnvironWarnings(object): + """ + os.environ keys and values should be native strings, otherwise it will cause problems with other modules (notably + subprocess). On Python 2 os.environ accepts anything without complaining, while Python 3 does the right thing + and raises an error. + """ + + VAR_NAME = u"PYTEST_INTERNAL_MY_VAR" + + @pytest.mark.skipif(six.PY3, reason="Python 2 only test") + def test_setenv_unicode_key(self, monkeypatch): + with pytest.warns( + pytest.PytestWarning, + match="Environment variable name {!r} should be str".format(self.VAR_NAME), + ): + monkeypatch.setenv(self.VAR_NAME, "2") + + @pytest.mark.skipif(six.PY3, reason="Python 2 only test") + def test_delenv_unicode_key(self, monkeypatch): + with pytest.warns( + pytest.PytestWarning, + match="Environment variable name {!r} should be str".format(self.VAR_NAME), + ): + monkeypatch.delenv(self.VAR_NAME, raising=False) + + def test_setenv_non_str_warning(self, monkeypatch): + value = 2 + msg = ( + "Environment variable value {!r} should be str, converted to str implicitly" + ) + with pytest.warns(pytest.PytestWarning, match=msg.format(value)): + monkeypatch.setenv(str(self.VAR_NAME), value) + + def test_setenv_prepend(): import os monkeypatch = MonkeyPatch() - monkeypatch.setenv("XYZ123", 2, prepend="-") + with pytest.warns(pytest.PytestWarning): + monkeypatch.setenv("XYZ123", 2, prepend="-") assert os.environ["XYZ123"] == "2" - monkeypatch.setenv("XYZ123", 3, prepend="-") + with pytest.warns(pytest.PytestWarning): + monkeypatch.setenv("XYZ123", 3, prepend="-") assert os.environ["XYZ123"] == "3-2" monkeypatch.undo() assert "XYZ123" not in os.environ diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cca704c4c..7651f3ab3 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -681,14 +681,22 @@ def test_pass_reporting_on_fail(testdir): def test_pass_output_reporting(testdir): testdir.makepyfile( """ - def test_pass_output(): + def test_pass_has_output(): print("Four score and seven years ago...") + def test_pass_no_output(): + pass """ ) result = testdir.runpytest() - assert "Four score and seven years ago..." not in result.stdout.str() + s = result.stdout.str() + assert "test_pass_has_output" not in s + assert "Four score and seven years ago..." not in s + assert "test_pass_no_output" not in s result = testdir.runpytest("-rP") - result.stdout.fnmatch_lines(["Four score and seven years ago..."]) + result.stdout.fnmatch_lines( + ["*test_pass_has_output*", "Four score and seven years ago..."] + ) + assert "test_pass_no_output" not in result.stdout.str() def test_color_yes(testdir):