Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/en/announce/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Release announcements
:maxdepth: 2


release-5.3.5
release-5.3.4
release-5.3.3
release-5.3.2
Expand Down
19 changes: 19 additions & 0 deletions doc/en/announce/release-5.3.5.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
pytest-5.3.5
=======================================

pytest 5.3.5 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:

* Daniel Hahler
* Ran Benita


Happy testing,
The pytest Development Team
9 changes: 9 additions & 0 deletions doc/en/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ with advance notice in the **Deprecations** section of releases.

.. towncrier release notes start

pytest 5.3.5 (2020-01-29)
=========================

Bug Fixes
---------

- `#6517 <https://github.com/pytest-dev/pytest/issues/6517>`_: Fix regression in pytest 5.3.4 causing an INTERNALERROR due to a wrong assertion.


pytest 5.3.4 (2020-01-20)
=========================

Expand Down
40 changes: 34 additions & 6 deletions doc/en/example/simple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -461,21 +461,49 @@ an ``incremental`` marker which is to be used on classes:

# content of conftest.py

import pytest
# store history of failures per test class name and per index in parametrize (if parametrize used)
_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}


def pytest_runtest_makereport(item, call):
if "incremental" in item.keywords:
# incremental marker is used
if call.excinfo is not None:
parent = item.parent
parent._previousfailed = item
# the test has failed
# retrieve the class name of the test
cls_name = str(item.cls)
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the test function
test_name = item.originalname or item.name
# store in _test_failed_incremental the original name of the failed test
_test_failed_incremental.setdefault(cls_name, {}).setdefault(
parametrize_index, test_name
)


def pytest_runtest_setup(item):
if "incremental" in item.keywords:
previousfailed = getattr(item.parent, "_previousfailed", None)
if previousfailed is not None:
pytest.xfail("previous test failed ({})".format(previousfailed.name))
# retrieve the class name of the test
cls_name = str(item.cls)
# check if a previous test has failed for this class
if cls_name in _test_failed_incremental:
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the first test function to fail for this class name and index
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
# if name found, test has failed for the combination of class name & test name
if test_name is not None:
pytest.xfail("previous test failed ({})".format(test_name))


These two hook implementations work together to abort incremental-marked
tests in a class. Here is a test module example:
Expand Down
2 changes: 1 addition & 1 deletion doc/en/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Once you develop multiple tests, you may want to group them into a class. pytest
x = "hello"
assert hasattr(x, "check")

``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything. We can simply run the module by passing its filename:
``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything, but make sure to prefix your class with ``Test`` otherwise the class will be skipped. We can simply run the module by passing its filename:

.. code-block:: pytest

Expand Down
2 changes: 1 addition & 1 deletion scripts/publish-gh-release-notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def parse_changelog(tag_name):


def convert_rst_to_md(text):
return pypandoc.convert_text(text, "md", format="rst")
return pypandoc.convert_text(text, "md", format="rst", extra_args=["--wrap=none"])


def main(argv):
Expand Down
47 changes: 24 additions & 23 deletions testing/code/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@
from types import FrameType
from unittest import mock

import _pytest._code
import pytest
from _pytest._code import Code
from _pytest._code import ExceptionInfo
from _pytest._code import Frame
from _pytest._code.code import ReprFuncArgs


def test_ne() -> None:
code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec"))
code1 = Code(compile('foo = "bar"', "", "exec"))
assert code1 == code1
code2 = _pytest._code.Code(compile('foo = "baz"', "", "exec"))
code2 = Code(compile('foo = "baz"', "", "exec"))
assert code2 != code1


def test_code_gives_back_name_for_not_existing_file() -> None:
name = "abc-123"
co_code = compile("pass\n", name, "exec")
assert co_code.co_filename == name
code = _pytest._code.Code(co_code)
code = Code(co_code)
assert str(code.path) == name
assert code.fullsource is None

Expand All @@ -26,21 +29,21 @@ def test_code_with_class() -> None:
class A:
pass

pytest.raises(TypeError, _pytest._code.Code, A)
pytest.raises(TypeError, Code, A)


def x() -> None:
raise NotImplementedError()


def test_code_fullsource() -> None:
code = _pytest._code.Code(x)
code = Code(x)
full = code.fullsource
assert "test_code_fullsource()" in str(full)


def test_code_source() -> None:
code = _pytest._code.Code(x)
code = Code(x)
src = code.source()
expected = """def x() -> None:
raise NotImplementedError()"""
Expand All @@ -51,7 +54,7 @@ def test_frame_getsourcelineno_myself() -> None:
def func() -> FrameType:
return sys._getframe(0)

f = _pytest._code.Frame(func())
f = Frame(func())
source, lineno = f.code.fullsource, f.lineno
assert source is not None
assert source[lineno].startswith(" return sys._getframe(0)")
Expand All @@ -61,13 +64,13 @@ def test_getstatement_empty_fullsource() -> None:
def func() -> FrameType:
return sys._getframe(0)

f = _pytest._code.Frame(func())
f = Frame(func())
with mock.patch.object(f.code.__class__, "fullsource", None):
assert f.statement == ""


def test_code_from_func() -> None:
co = _pytest._code.Code(test_frame_getsourcelineno_myself)
co = Code(test_frame_getsourcelineno_myself)
assert co.firstlineno
assert co.path

Expand All @@ -86,51 +89,51 @@ def test_code_getargs() -> None:
def f1(x):
raise NotImplementedError()

c1 = _pytest._code.Code(f1)
c1 = Code(f1)
assert c1.getargs(var=True) == ("x",)

def f2(x, *y):
raise NotImplementedError()

c2 = _pytest._code.Code(f2)
c2 = Code(f2)
assert c2.getargs(var=True) == ("x", "y")

def f3(x, **z):
raise NotImplementedError()

c3 = _pytest._code.Code(f3)
c3 = Code(f3)
assert c3.getargs(var=True) == ("x", "z")

def f4(x, *y, **z):
raise NotImplementedError()

c4 = _pytest._code.Code(f4)
c4 = Code(f4)
assert c4.getargs(var=True) == ("x", "y", "z")


def test_frame_getargs() -> None:
def f1(x) -> FrameType:
return sys._getframe(0)

fr1 = _pytest._code.Frame(f1("a"))
fr1 = Frame(f1("a"))
assert fr1.getargs(var=True) == [("x", "a")]

def f2(x, *y) -> FrameType:
return sys._getframe(0)

fr2 = _pytest._code.Frame(f2("a", "b", "c"))
fr2 = Frame(f2("a", "b", "c"))
assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))]

def f3(x, **z) -> FrameType:
return sys._getframe(0)

fr3 = _pytest._code.Frame(f3("a", b="c"))
fr3 = Frame(f3("a", b="c"))
assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})]

def f4(x, *y, **z) -> FrameType:
return sys._getframe(0)

fr4 = _pytest._code.Frame(f4("a", "b", c="d"))
fr4 = Frame(f4("a", "b", c="d"))
assert fr4.getargs(var=True) == [("x", "a"), ("y", ("b",)), ("z", {"c": "d"})]


Expand All @@ -142,12 +145,12 @@ def test_bad_getsource(self) -> None:
else:
assert False
except AssertionError:
exci = _pytest._code.ExceptionInfo.from_current()
exci = ExceptionInfo.from_current()
assert exci.getrepr()

def test_from_current_with_missing(self) -> None:
with pytest.raises(AssertionError, match="no current exception"):
_pytest._code.ExceptionInfo.from_current()
ExceptionInfo.from_current()


class TestTracebackEntry:
Expand All @@ -158,7 +161,7 @@ def test_getsource(self) -> None:
else:
assert False
except AssertionError:
exci = _pytest._code.ExceptionInfo.from_current()
exci = ExceptionInfo.from_current()
entry = exci.traceback[0]
source = entry.getsource()
assert source is not None
Expand All @@ -168,8 +171,6 @@ def test_getsource(self) -> None:

class TestReprFuncArgs:
def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None:
from _pytest._code.code import ReprFuncArgs

args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")]

r = ReprFuncArgs(args)
Expand Down
8 changes: 4 additions & 4 deletions testing/code/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from typing import Dict
from typing import Optional

import py
import py.path

import _pytest._code
import pytest
from _pytest._code import getfslineno
from _pytest._code import Source


Expand Down Expand Up @@ -496,10 +497,8 @@ def x():


def test_getfslineno() -> None:
from _pytest._code import getfslineno

def f(x) -> None:
pass
raise NotImplementedError()

fspath, lineno = getfslineno(f)

Expand All @@ -513,6 +512,7 @@ class A:
fspath, lineno = getfslineno(A)

_, A_lineno = inspect.findsource(A)
assert isinstance(fspath, py.path.local)
assert fspath.basename == "test_source.py"
assert lineno == A_lineno

Expand Down
21 changes: 14 additions & 7 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
}
RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}

TRANS_FNMATCH = str.maketrans({"[": "[[]", "]": "[]]"})


class Option:
def __init__(self, verbosity=0):
Expand Down Expand Up @@ -1852,14 +1854,19 @@ def test_teardown_many(self, testdir, many_files):
[r"test_bar.py (\.E){5}\s+\[ 25%\]", r"test_foo.py (\.E){15}\s+\[100%\]"]
)

def test_teardown_many_verbose(self, testdir, many_files):
output = testdir.runpytest("-v")
output.stdout.re_match_lines(
def test_teardown_many_verbose(self, testdir: Testdir, many_files) -> None:
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines(
[
r"test_bar.py::test_bar\[0\] PASSED\s+\[ 5%\]",
r"test_bar.py::test_bar\[0\] ERROR\s+\[ 5%\]",
r"test_bar.py::test_bar\[4\] PASSED\s+\[ 25%\]",
r"test_bar.py::test_bar\[4\] ERROR\s+\[ 25%\]",
line.translate(TRANS_FNMATCH)
for line in [
"test_bar.py::test_bar[0] PASSED * [ 5%]",
"test_bar.py::test_bar[0] ERROR * [ 5%]",
"test_bar.py::test_bar[4] PASSED * [ 25%]",
"test_foo.py::test_foo[14] PASSED * [100%]",
"test_foo.py::test_foo[14] ERROR * [100%]",
"=* 20 passed, 20 errors in *",
]
]
)

Expand Down