diff options
-rw-r--r-- | .pre-commit-config.yaml | 2 | ||||
-rw-r--r-- | changelog/8152.bugfix.rst | 1 | ||||
-rw-r--r-- | changelog/8249.bugfix.rst | 1 | ||||
-rw-r--r-- | doc/en/announce/index.rst | 1 | ||||
-rw-r--r-- | doc/en/announce/release-6.2.2.rst | 21 | ||||
-rw-r--r-- | doc/en/changelog.rst | 12 | ||||
-rw-r--r-- | doc/en/example/parametrize.rst | 4 | ||||
-rw-r--r-- | doc/en/getting-started.rst | 4 | ||||
-rw-r--r-- | doc/en/index.rst | 1 | ||||
-rw-r--r-- | doc/en/reference.rst | 2 | ||||
-rw-r--r-- | doc/en/usage.rst | 2 | ||||
-rw-r--r-- | src/_pytest/fixtures.py | 5 | ||||
-rw-r--r-- | src/_pytest/pytester.py | 8 | ||||
-rw-r--r-- | src/_pytest/runner.py | 183 | ||||
-rw-r--r-- | src/_pytest/threadexception.py | 7 | ||||
-rw-r--r-- | testing/plugins_integration/requirements.txt | 2 | ||||
-rw-r--r-- | testing/python/fixtures.py | 17 | ||||
-rw-r--r-- | testing/test_runner.py | 44 |
18 files changed, 209 insertions, 108 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8e19b283..9130a79a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 + rev: v0.800 hooks: - id: mypy files: ^(src/|testing/) diff --git a/changelog/8152.bugfix.rst b/changelog/8152.bugfix.rst deleted file mode 100644 index d79a832de..000000000 --- a/changelog/8152.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed "(<Skipped instance>)" being shown as a skip reason in the verbose test summary line when the reason is empty. diff --git a/changelog/8249.bugfix.rst b/changelog/8249.bugfix.rst deleted file mode 100644 index aa084c757..000000000 --- a/changelog/8249.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e7cac2a1c..a7656c5ee 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-6.2.2 release-6.2.1 release-6.2.0 release-6.1.2 diff --git a/doc/en/announce/release-6.2.2.rst b/doc/en/announce/release-6.2.2.rst new file mode 100644 index 000000000..c3999c538 --- /dev/null +++ b/doc/en/announce/release-6.2.2.rst @@ -0,0 +1,21 @@ +pytest-6.2.2 +======================================= + +pytest 6.2.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/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Adam Johnson +* Bruno Oliveira +* Chris NeJame +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 6d66ad1d8..3e854f599 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,18 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 6.2.2 (2021-01-25) +========================= + +Bug Fixes +--------- + +- `#8152 <https://github.com/pytest-dev/pytest/issues/8152>`_: Fixed "(<Skipped instance>)" being shown as a skip reason in the verbose test summary line when the reason is empty. + + +- `#8249 <https://github.com/pytest-dev/pytest/issues/8249>`_: Fix the ``faulthandler`` plugin for occasions when running with ``twisted.logger`` and using ``pytest --capture=no``. + + pytest 6.2.1 (2020-12-15) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index a65ee5f2f..771c7e16f 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -637,13 +637,13 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python cachedir: $PYTHON_PREFIX/.pytest_cache rootdir: $REGENDOC_TMPDIR - collecting ... collected 14 items / 11 deselected / 3 selected + collecting ... collected 24 items / 21 deselected / 3 selected test_pytest_param_example.py::test_eval[1+7-8] PASSED [ 33%] test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%] test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%] - =============== 2 passed, 11 deselected, 1 xfailed in 0.12s ================ + =============== 2 passed, 21 deselected, 1 xfailed in 0.12s ================ As the result: diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 09410585d..28fd862cf 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 6.2.1 + pytest 6.2.2 .. _`simpletest`: @@ -210,6 +210,8 @@ This is outlined below: FAILED test_class_demo.py::TestClassDemoInstance::test_two - assert 0 2 failed in 0.12s +Note that attributes added at class level are *class attributes*, so they will be shared between tests. + Request a unique temporary directory for functional tests -------------------------------------------------------------- diff --git a/doc/en/index.rst b/doc/en/index.rst index 58f6c1d86..7c4d9394d 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -11,6 +11,7 @@ pytest: helps you write better programs ======================================= +.. module:: pytest The ``pytest`` framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 51c52b33a..bc6c5670a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -3,8 +3,6 @@ API Reference ============= -.. module:: pytest - This page contains the full reference to pytest's API. .. contents:: diff --git a/doc/en/usage.rst b/doc/en/usage.rst index fbd3333da..0a26182d4 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -497,7 +497,7 @@ The plugins are automatically enabled for pytest runs, unless the ``-p no:threadexception`` (for thread exceptions) options are given on the command-line. -The warnings may be silenced selectivly using the :ref:`pytest.mark.filterwarnings ref` +The warnings may be silenced selectively using the :ref:`pytest.mark.filterwarnings ref` mark. The warning categories are :class:`pytest.PytestUnraisableExceptionWarning` and :class:`pytest.PytestUnhandledThreadExceptionWarning`. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 43a40a864..269369642 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -372,6 +372,7 @@ def _fill_fixtures_impl(function: "Function") -> None: fi = fm.getfixtureinfo(function.parent, function.obj, None) function._fixtureinfo = fi request = function._request = FixtureRequest(function, _ispytest=True) + fm.session._setupstate.prepare(function) request._fillfixtures() # Prune out funcargs for jstests. newfuncargs = {} @@ -543,8 +544,8 @@ class FixtureRequest: self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None: - item = self._getscopeitem(scope) - item.addfinalizer(finalizer) + node = self._getscopeitem(scope) + node.addfinalizer(finalizer) def applymarker(self, marker: Union[str, MarkDecorator]) -> None: """Apply a marker to a single test function invocation. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 95b22b3b2..8ca21d1c5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -777,7 +777,7 @@ class Pytester: return ret def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: - r"""Create new file(s) in the test directory. + r"""Create new text file(s) in the test directory. :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. @@ -797,6 +797,12 @@ class Pytester: pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") + To create binary files, use :meth:`pathlib.Path.write_bytes` directly: + + .. code-block:: python + + filename = pytester.path.joinpath("foo.bin") + filename.write_bytes(b"...") """ return self._makefile(ext, args, kwargs) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 844e41f80..ae76a2472 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -33,6 +33,7 @@ from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.nodes import Node from _pytest.outcomes import Exit +from _pytest.outcomes import OutcomeException from _pytest.outcomes import Skipped from _pytest.outcomes import TEST_OUTCOME @@ -103,7 +104,7 @@ def pytest_sessionstart(session: "Session") -> None: def pytest_sessionfinish(session: "Session") -> None: - session._setupstate.teardown_all() + session._setupstate.teardown_exact(None) def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: @@ -175,7 +176,7 @@ def pytest_runtest_call(item: Item) -> None: def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: _update_current_test_var(item, "teardown") - item.session._setupstate.teardown_exact(item, nextitem) + item.session._setupstate.teardown_exact(nextitem) _update_current_test_var(item, None) @@ -401,88 +402,136 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: class SetupState: - """Shared state for setting up/tearing down test items or collectors.""" + """Shared state for setting up/tearing down test items or collectors + in a session. - def __init__(self): - self.stack: List[Node] = [] - self._finalizers: Dict[Node, List[Callable[[], object]]] = {} + Suppose we have a collection tree as follows: - def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: - """Attach a finalizer to the given colitem.""" - assert colitem and not isinstance(colitem, tuple) - assert callable(finalizer) - # assert colitem in self.stack # some unit tests don't setup stack :/ - self._finalizers.setdefault(colitem, []).append(finalizer) + <Session session> + <Module mod1> + <Function item1> + <Module mod2> + <Function item2> - def _pop_and_teardown(self): - colitem = self.stack.pop() - self._teardown_with_finalization(colitem) + The SetupState maintains a stack. The stack starts out empty: - def _callfinalizers(self, colitem) -> None: - finalizers = self._finalizers.pop(colitem, None) - exc = None - while finalizers: - fin = finalizers.pop() - try: - fin() - except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = e - if exc: - raise exc + [] - def _teardown_with_finalization(self, colitem) -> None: - self._callfinalizers(colitem) - colitem.teardown() - for colitem in self._finalizers: - assert colitem in self.stack + During the setup phase of item1, prepare(item1) is called. What it does + is: - def teardown_all(self) -> None: - while self.stack: - self._pop_and_teardown() - for key in list(self._finalizers): - self._teardown_with_finalization(key) - assert not self._finalizers + push session to stack, run session.setup() + push mod1 to stack, run mod1.setup() + push item1 to stack, run item1.setup() - def teardown_exact(self, item, nextitem) -> None: - needed_collectors = nextitem and nextitem.listchain() or [] - self._teardown_towards(needed_collectors) + The stack is: - def _teardown_towards(self, needed_collectors) -> None: - exc = None - while self.stack: - if self.stack == needed_collectors[: len(self.stack)]: - break - try: - self._pop_and_teardown() - except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = e - if exc: - raise exc + [session, mod1, item1] + + While the stack is in this shape, it is allowed to add finalizers to + each of session, mod1, item1 using addfinalizer(). + + During the teardown phase of item1, teardown_exact(item2) is called, + where item2 is the next item to item1. What it does is: + + pop item1 from stack, run its teardowns + pop mod1 from stack, run its teardowns + + mod1 was popped because it ended its purpose with item1. The stack is: + + [session] + + During the setup phase of item2, prepare(item2) is called. What it does + is: - def prepare(self, colitem) -> None: - """Setup objects along the collector chain to the test-method.""" + push mod2 to stack, run mod2.setup() + push item2 to stack, run item2.setup() - # Check if the last collection node has raised an error. - for col in self.stack: - if hasattr(col, "_prepare_exc"): - exc = col._prepare_exc # type: ignore[attr-defined] - raise exc + Stack: - needed_collectors = colitem.listchain() + [session, mod2, item2] + + During the teardown phase of item2, teardown_exact(None) is called, + because item2 is the last item. What it does is: + + pop item2 from stack, run its teardowns + pop mod2 from stack, run its teardowns + pop session from stack, run its teardowns + + Stack: + + [] + + The end! + """ + + def __init__(self) -> None: + # The stack is in the dict insertion order. + self.stack: Dict[ + Node, + Tuple[ + # Node's finalizers. + List[Callable[[], object]], + # Node's exception, if its setup raised. + Optional[Union[OutcomeException, Exception]], + ], + ] = {} + + def prepare(self, item: Item) -> None: + """Setup objects along the collector chain to the item.""" + # If a collector fails its setup, fail its entire subtree of items. + # The setup is not retried for each item - the same exception is used. + for col, (finalizers, prepare_exc) in self.stack.items(): + if prepare_exc: + raise prepare_exc + + needed_collectors = item.listchain() for col in needed_collectors[len(self.stack) :]: - self.stack.append(col) + assert col not in self.stack + self.stack[col] = ([col.teardown], None) try: col.setup() except TEST_OUTCOME as e: - col._prepare_exc = e # type: ignore[attr-defined] + self.stack[col] = (self.stack[col][0], e) raise e + def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: + """Attach a finalizer to the given node. + + The node must be currently active in the stack. + """ + assert node and not isinstance(node, tuple) + assert callable(finalizer) + assert node in self.stack, (node, self.stack) + self.stack[node][0].append(finalizer) + + def teardown_exact(self, nextitem: Optional[Item]) -> None: + """Teardown the current stack up until reaching nodes that nextitem + also descends from. + + When nextitem is None (meaning we're at the last item), the entire + stack is torn down. + """ + needed_collectors = nextitem and nextitem.listchain() or [] + exc = None + while self.stack: + if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: + break + node, (finalizers, prepare_exc) = self.stack.popitem() + while finalizers: + fin = finalizers.pop() + try: + fin() + except TEST_OUTCOME as e: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if exc is None: + exc = e + if exc: + raise exc + if nextitem is None: + assert not self.stack + def collect_one_node(collector: Collector) -> CollectReport: ihook = collector.ihook diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index d084dc6e6..b250a5234 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -34,11 +34,10 @@ class catch_threading_exception: """ def __init__(self) -> None: - # See https://github.com/python/typeshed/issues/4767 regarding the underscore. - self.args: Optional["threading._ExceptHookArgs"] = None - self._old_hook: Optional[Callable[["threading._ExceptHookArgs"], Any]] = None + self.args: Optional["threading.ExceptHookArgs"] = None + self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None - def _hook(self, args: "threading._ExceptHookArgs") -> None: + def _hook(self, args: "threading.ExceptHookArgs") -> None: self.args = args def __enter__(self) -> "catch_threading_exception": diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index ae5c9a93f..86c2a862c 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -2,7 +2,7 @@ anyio[curio,trio]==2.0.2 django==3.1.5 pytest-asyncio==0.14.0 pytest-bdd==4.0.2 -pytest-cov==2.10.1 +pytest-cov==2.11.1 pytest-django==4.1.0 pytest-flakes==4.0.3 pytest-html==3.1.1 diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 12340e690..3d78ebf58 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -130,7 +130,8 @@ class TestFillFixtures: pytester.copy_example() item = pytester.getitem(Path("test_funcarg_basic.py")) assert isinstance(item, Function) - item._request._fillfixtures() + # Execute's item's setup, which fills fixtures. + item.session._setupstate.prepare(item) del item.funcargs["request"] assert len(get_public_names(item.funcargs)) == 2 assert item.funcargs["some"] == "test_func" @@ -809,18 +810,25 @@ class TestRequestBasic: item = pytester.getitem( """ import pytest - values = [2] + @pytest.fixture - def something(request): return 1 + def something(request): + return 1 + + values = [2] @pytest.fixture def other(request): return values.pop() + def test_func(something): pass """ ) assert isinstance(item, Function) req = item._request + # Execute item's setup. + item.session._setupstate.prepare(item) + with pytest.raises(pytest.FixtureLookupError): req.getfixturevalue("notexists") val = req.getfixturevalue("something") @@ -831,7 +839,6 @@ class TestRequestBasic: assert val2 == 2 val2 = req.getfixturevalue("other") # see about caching assert val2 == 2 - item._request._fillfixtures() assert item.funcargs["something"] == 1 assert len(get_public_names(item.funcargs)) == 2 assert "request" in item.funcargs @@ -856,7 +863,7 @@ class TestRequestBasic: teardownlist = parent.obj.teardownlist ss = item.session._setupstate assert not teardownlist - ss.teardown_exact(item, None) + ss.teardown_exact(None) print(ss.stack) assert teardownlist == [1] diff --git a/testing/test_runner.py b/testing/test_runner.py index 8ce0f6735..e3f286307 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -22,21 +22,22 @@ from _pytest.pytester import Pytester class TestSetupState: def test_setup(self, pytester: Pytester) -> None: - ss = runner.SetupState() item = pytester.getitem("def test_func(): pass") + ss = item.session._setupstate values = [1] ss.prepare(item) - ss.addfinalizer(values.pop, colitem=item) + ss.addfinalizer(values.pop, item) assert values - ss._pop_and_teardown() + ss.teardown_exact(None) assert not values def test_teardown_exact_stack_empty(self, pytester: Pytester) -> None: item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() - ss.teardown_exact(item, None) - ss.teardown_exact(item, None) - ss.teardown_exact(item, None) + ss = item.session._setupstate + ss.prepare(item) + ss.teardown_exact(None) + ss.teardown_exact(None) + ss.teardown_exact(None) def test_setup_fails_and_failure_is_cached(self, pytester: Pytester) -> None: item = pytester.getitem( @@ -46,9 +47,11 @@ class TestSetupState: def test_func(): pass """ ) - ss = runner.SetupState() - pytest.raises(ValueError, lambda: ss.prepare(item)) - pytest.raises(ValueError, lambda: ss.prepare(item)) + ss = item.session._setupstate + with pytest.raises(ValueError): + ss.prepare(item) + with pytest.raises(ValueError): + ss.prepare(item) def test_teardown_multiple_one_fails(self, pytester: Pytester) -> None: r = [] @@ -63,12 +66,13 @@ class TestSetupState: r.append("fin3") item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() + ss = item.session._setupstate + ss.prepare(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) ss.addfinalizer(fin3, item) with pytest.raises(Exception) as err: - ss._callfinalizers(item) + ss.teardown_exact(None) assert err.value.args == ("oops",) assert r == ["fin3", "fin1"] @@ -82,11 +86,12 @@ class TestSetupState: raise Exception("oops2") item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() + ss = item.session._setupstate + ss.prepare(item) ss.addfinalizer(fin1, item) ss.addfinalizer(fin2, item) with pytest.raises(Exception) as err: - ss._callfinalizers(item) + ss.teardown_exact(None) assert err.value.args == ("oops2",) def test_teardown_multiple_scopes_one_fails(self, pytester: Pytester) -> None: @@ -99,13 +104,14 @@ class TestSetupState: module_teardown.append("fin_module") item = pytester.getitem("def test_func(): pass") - ss = runner.SetupState() - ss.addfinalizer(fin_module, item.listchain()[-2]) - ss.addfinalizer(fin_func, item) + mod = item.listchain()[-2] + ss = item.session._setupstate ss.prepare(item) + ss.addfinalizer(fin_module, mod) + ss.addfinalizer(fin_func, item) with pytest.raises(Exception, match="oops1"): - ss.teardown_exact(item, None) - assert module_teardown + ss.teardown_exact(None) + assert module_teardown == ["fin_module"] class BaseFunctionalTests: |