Merge pull request #6478 from blueyed/merge-master-into-features

Merge master into features
This commit is contained in:
Daniel Hahler 2020-01-16 21:14:30 +01:00 committed by GitHub
commit a4f5b8a4d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 318 additions and 38 deletions

View File

@ -24,3 +24,5 @@ exclude_lines =
\#\s*pragma: no cover
^\s*raise NotImplementedError\b
^\s*return NotImplemented\b
^\s*if TYPE_CHECKING:

View File

@ -11,6 +11,9 @@ on:
branches:
- master
- features
tags:
- "*"
pull_request:
branches:
- master
@ -156,3 +159,35 @@ jobs:
flags: ${{ runner.os }}
fail_ci_if_error: false
name: ${{ matrix.name }}
deploy:
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: "3.7"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade wheel setuptools tox
- name: Build package
run: |
python setup.py sdist bdist_wheel
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_token }}
- name: Publish GitHub release notes
env:
GH_RELEASE_NOTES_TOKEN: ${{ secrets.release_notes }}
run: |
sudo apt-get install pandoc
tox -e publish-gh-release-notes

View File

@ -37,7 +37,7 @@ repos:
- id: pyupgrade
args: [--py3-plus]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.761
rev: v0.761 # NOTE: keep this in sync with setup.py.
hooks:
- id: mypy
files: ^(src/|testing/)

View File

@ -55,6 +55,7 @@ Charles Cloud
Charles Machalow
Charnjit SiNGH (CCSJ)
Chris Lamb
Chris NeJame
Christian Boelsen
Christian Fetzer
Christian Neumüller

View File

@ -0,0 +1 @@
Captured output during teardown is shown with ``-rP``.

View File

@ -0,0 +1,3 @@
:class:`FixtureDef <_pytest.fixtures.FixtureDef>` objects now properly register their finalizers with autouse and
parameterized fixtures that execute before them in the fixture stack so they are torn
down at the right times, and in the right order.

View File

@ -19,8 +19,9 @@ import os
import sys
from _pytest import __version__ as version
from _pytest.compat import TYPE_CHECKING
if False: # TYPE_CHECKING
if TYPE_CHECKING:
import sphinx.application

View File

@ -68,19 +68,21 @@ def main(argv):
if len(argv) > 1:
tag_name = argv[1]
else:
tag_name = os.environ.get("TRAVIS_TAG")
tag_name = os.environ.get("GITHUB_REF")
if not tag_name:
print("tag_name not given and $TRAVIS_TAG not set", file=sys.stderr)
print("tag_name not given and $GITHUB_REF not set", file=sys.stderr)
return 1
if tag_name.startswith("refs/tags/"):
tag_name = tag_name[len("refs/tags/") :]
token = os.environ.get("GH_RELEASE_NOTES_TOKEN")
if not token:
print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr)
return 1
slug = os.environ.get("TRAVIS_REPO_SLUG")
slug = os.environ.get("GITHUB_REPOSITORY")
if not slug:
print("TRAVIS_REPO_SLUG not set", file=sys.stderr)
print("GITHUB_REPOSITORY not set", file=sys.stderr)
return 1
rst_body = parse_changelog(tag_name)

View File

@ -29,7 +29,10 @@ def main():
"nose",
"requests",
"xmlschema",
]
],
"checkqa-mypy": [
"mypy==v0.761", # keep this in sync with .pre-commit-config.yaml.
],
},
install_requires=INSTALL_REQUIRES,
)

View File

@ -32,8 +32,9 @@ import _pytest
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type
from typing_extensions import Literal
from weakref import ReferenceType # noqa: F401

View File

@ -28,7 +28,13 @@ from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
if False: # TYPE_CHECKING
if sys.version_info < (3, 5, 2):
TYPE_CHECKING = False # type: bool
else:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type # noqa: F401 (used in type string)

View File

@ -37,12 +37,13 @@ from .findpaths import exists
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest.compat import importlib_metadata
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import Path
from _pytest.warning_types import PytestConfigWarning
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type
from .argparsing import Argument

View File

@ -15,9 +15,10 @@ from typing import Union
import py
from _pytest.compat import TYPE_CHECKING
from _pytest.config.exceptions import UsageError
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import NoReturn
from typing_extensions import Literal # noqa: F401

View File

@ -5,9 +5,10 @@ from typing import Optional
import py
from .exceptions import UsageError
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
if False:
if TYPE_CHECKING:
from . import Config # noqa: F401

View File

@ -19,12 +19,13 @@ from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr
from _pytest.compat import safe_getattr
from _pytest.compat import TYPE_CHECKING
from _pytest.fixtures import FixtureRequest
from _pytest.outcomes import Skipped
from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning
if False: # TYPE_CHECKING
if TYPE_CHECKING:
import doctest
from typing import Type

View File

@ -27,12 +27,13 @@ from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.compat import TYPE_CHECKING
from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS
from _pytest.deprecated import FUNCARGNAMES
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type
from _pytest import nodes
@ -880,9 +881,7 @@ class FixtureDef:
self._finalizers = []
def execute(self, request):
# get required arguments and register our own finish()
# with their finalization
for argname in self.argnames:
for argname in self._dependee_fixture_argnames(request):
fixturedef = request._get_active_fixturedef(argname)
if argname != "request":
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
@ -905,6 +904,61 @@ class FixtureDef:
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
return hook.pytest_fixture_setup(fixturedef=self, request=request)
def _dependee_fixture_argnames(self, request):
"""A list of argnames for fixtures that this fixture depends on.
Given a request, this looks at the currently known list of fixture argnames, and
attempts to determine what slice of the list contains fixtures that it can know
should execute before it. This information is necessary so that this fixture can
know what fixtures to register its finalizer with to make sure that if they
would be torn down, they would tear down this fixture before themselves. It's
crucial for fixtures to be torn down in the inverse order that they were set up
in so that they don't try to clean up something that another fixture is still
depending on.
When autouse fixtures are involved, it can be tricky to figure out when fixtures
should be torn down. To solve this, this method leverages the ``fixturenames``
list provided by the ``request`` object, as this list is at least somewhat
sorted (in terms of the order fixtures are set up in) by the time this method is
reached. It's sorted enough that the starting point of fixtures that depend on
this one can be found using the ``self._parent_request`` stack.
If a request in the ``self._parent_request`` stack has a ``:class:FixtureDef``
associated with it, then that fixture is dependent on this one, so any fixture
names that appear in the list of fixture argnames that come after it can also be
ruled out. The argnames of all fixtures associated with a request in the
``self._parent_request`` stack are found, and the lowest index argname is
considered the earliest point in the list of fixture argnames where everything
from that point onward can be considered to execute after this fixture.
Everything before this point can be considered fixtures that this fixture
depends on, and so this fixture should register its finalizer with all of them
to ensure that if any of them are to be torn down, they will tear this fixture
down first.
This is the first part of the list of fixture argnames that is returned. The last
part of the list is everything in ``self.argnames`` as those are explicit
dependees of this fixture, so this fixture should definitely register its
finalizer with them.
"""
all_fix_names = request.fixturenames
try:
current_fix_index = all_fix_names.index(self.argname)
except ValueError:
current_fix_index = len(request.fixturenames)
parent_fixture_indexes = set()
parent_request = request._parent_request
while hasattr(parent_request, "_parent_request"):
if hasattr(parent_request, "_fixturedef"):
parent_fix_name = parent_request._fixturedef.argname
if parent_fix_name in all_fix_names:
parent_fixture_indexes.add(all_fix_names.index(parent_fix_name))
parent_request = parent_request._parent_request
stack_slice_index = min([current_fix_index, *parent_fixture_indexes])
active_fixture_argnames = all_fix_names[:stack_slice_index]
return {*active_fixture_argnames, *self.argnames}
def cache_key(self, request):
return request.param_index if not hasattr(request, "param") else request.param

View File

@ -17,6 +17,7 @@ from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprExceptionInfo
from _pytest.compat import cached_property
from _pytest.compat import getfslineno
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.deprecated import NODE_USE_FROM_PARENT
from _pytest.fixtures import FixtureDef
@ -27,7 +28,7 @@ from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import Failed
if False: # TYPE_CHECKING
if TYPE_CHECKING:
# Imported here due to circular import.
from _pytest.main import Session # noqa: F401
@ -50,7 +51,7 @@ def _splitnode(nodeid):
[]
['testing', 'code']
['testing', 'code', 'test_excinfo.py']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()']
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
"""
if nodeid == "":
# If there is no root node at all, return an empty list so the caller's logic can remain sane

View File

@ -8,7 +8,9 @@ from typing import Optional
from packaging.version import Version
if False: # TYPE_CHECKING
TYPE_CHECKING = False # avoid circular import through compat
if TYPE_CHECKING:
from typing import NoReturn

View File

@ -28,6 +28,7 @@ from _pytest._code import Source
from _pytest._io.saferepr import saferepr
from _pytest.capture import MultiCapture
from _pytest.capture import SysCapture
from _pytest.compat import TYPE_CHECKING
from _pytest.fixtures import FixtureRequest
from _pytest.main import ExitCode
from _pytest.main import Session
@ -35,7 +36,7 @@ from _pytest.monkeypatch import MonkeyPatch
from _pytest.pathlib import Path
from _pytest.reports import TestReport
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type
@ -189,7 +190,7 @@ class ParsedCall:
del d["_name"]
return "<ParsedCall {!r}(**{!r})>".format(self._name, d)
if False: # TYPE_CHECKING
if TYPE_CHECKING:
# The class has undetermined attributes, this tells mypy about it.
def __getattr__(self, key):
raise NotImplementedError()

View File

@ -23,9 +23,10 @@ from more_itertools.more import always_iterable
import _pytest._code
from _pytest.compat import overload
from _pytest.compat import STRING_TYPES
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type # noqa: F401 (used in type string)

View File

@ -12,10 +12,11 @@ from typing import Tuple
from typing import Union
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.fixtures import yield_fixture
from _pytest.outcomes import fail
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type

View File

@ -15,12 +15,13 @@ from .reports import CollectErrorRepr
from .reports import CollectReport
from .reports import TestReport
from _pytest._code.code import ExceptionInfo
from _pytest.compat import TYPE_CHECKING
from _pytest.nodes import Node
from _pytest.outcomes import Exit
from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type
#

View File

@ -834,8 +834,20 @@ class TerminalReporter:
msg = self._getfailureheadline(rep)
self.write_sep("_", msg, green=True, bold=True)
self._outrep_summary(rep)
self._handle_teardown_sections(rep.nodeid)
def print_teardown_sections(self, rep):
def _get_teardown_reports(self, nodeid: str) -> List[TestReport]:
return [
report
for report in self.getreports("")
if report.when == "teardown" and report.nodeid == nodeid
]
def _handle_teardown_sections(self, nodeid: str) -> None:
for report in self._get_teardown_reports(nodeid):
self.print_teardown_sections(report)
def print_teardown_sections(self, rep: TestReport) -> None:
showcapture = self.config.option.showcapture
if showcapture == "no":
return
@ -859,17 +871,11 @@ class TerminalReporter:
line = self._getcrashline(rep)
self.write_line(line)
else:
teardown_sections = {}
for report in self.getreports(""):
if report.when == "teardown":
teardown_sections.setdefault(report.nodeid, []).append(report)
for rep in reports:
msg = self._getfailureheadline(rep)
self.write_sep("_", msg, red=True, bold=True)
self._outrep_summary(rep)
for report in teardown_sections.get(rep.nodeid, []):
self.print_teardown_sections(report)
self._handle_teardown_sections(rep.nodeid)
def summary_errors(self):
if self.config.option.tbstyle != "no":

View File

@ -4,8 +4,9 @@ from typing import TypeVar
import attr
from _pytest.compat import TYPE_CHECKING
if False: # TYPE_CHECKING
if TYPE_CHECKING:
from typing import Type # noqa: F401 (used in type string)

View File

@ -1716,6 +1716,138 @@ class TestAutouseDiscovery:
reprec.assertoutcome(passed=3)
class TestMultiLevelAutouseAndParameterization:
def test_setup_and_teardown_order(self, testdir):
"""Tests that parameterized fixtures effect subsequent fixtures. (#6436)
If a fixture uses a parameterized fixture, or, for any other reason, is executed
after the parameterized fixture in the fixture stack, then it should be affected
by the parameterization, and as a result, should be torn down before the
parameterized fixture, every time the parameterized fixture is torn down. This
should be the case even if autouse is involved and/or the linear order of
fixture execution isn't deterministic. In other words, before any fixture can be
torn down, every fixture that was executed after it must also be torn down.
"""
testdir.makepyfile(
test_auto="""
import pytest
def f(param):
return param
@pytest.fixture(scope="session", autouse=True)
def s_fix(request):
yield
@pytest.fixture(scope="package", params=["p1", "p2"], ids=f, autouse=True)
def p_fix(request):
yield
@pytest.fixture(scope="module", params=["m1", "m2"], ids=f, autouse=True)
def m_fix(request):
yield
@pytest.fixture(scope="class", autouse=True)
def another_c_fix(m_fix):
yield
@pytest.fixture(scope="class")
def c_fix():
yield
@pytest.fixture(scope="function", params=["f1", "f2"], ids=f, autouse=True)
def f_fix(request):
yield
class TestFixtures:
def test_a(self, c_fix):
pass
def test_b(self, c_fix):
pass
"""
)
result = testdir.runpytest("--setup-plan")
test_fixtures_used = (
"(fixtures used: another_c_fix, c_fix, f_fix, m_fix, p_fix, request, s_fix)"
)
result.stdout.fnmatch_lines(
"""
SETUP S s_fix
SETUP P p_fix[p1]
SETUP M m_fix[m1]
SETUP C another_c_fix (fixtures used: m_fix)
SETUP C c_fix
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_a[p1-m1-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_a[p1-m1-f2] {0}
TEARDOWN F f_fix[f2]
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_b[p1-m1-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_b[p1-m1-f2] {0}
TEARDOWN F f_fix[f2]
TEARDOWN C c_fix
TEARDOWN C another_c_fix
TEARDOWN M m_fix[m1]
SETUP M m_fix[m2]
SETUP C another_c_fix (fixtures used: m_fix)
SETUP C c_fix
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_a[p1-m2-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_a[p1-m2-f2] {0}
TEARDOWN F f_fix[f2]
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_b[p1-m2-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_b[p1-m2-f2] {0}
TEARDOWN F f_fix[f2]
TEARDOWN C c_fix
TEARDOWN C another_c_fix
TEARDOWN M m_fix[m2]
TEARDOWN P p_fix[p1]
SETUP P p_fix[p2]
SETUP M m_fix[m1]
SETUP C another_c_fix (fixtures used: m_fix)
SETUP C c_fix
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_a[p2-m1-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_a[p2-m1-f2] {0}
TEARDOWN F f_fix[f2]
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_b[p2-m1-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_b[p2-m1-f2] {0}
TEARDOWN F f_fix[f2]
TEARDOWN C c_fix
TEARDOWN C another_c_fix
TEARDOWN M m_fix[m1]
SETUP M m_fix[m2]
SETUP C another_c_fix (fixtures used: m_fix)
SETUP C c_fix
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_a[p2-m2-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_a[p2-m2-f2] {0}
TEARDOWN F f_fix[f2]
SETUP F f_fix[f1]
test_auto.py::TestFixtures::test_b[p2-m2-f1] {0}
TEARDOWN F f_fix[f1]
SETUP F f_fix[f2]
test_auto.py::TestFixtures::test_b[p2-m2-f2] {0}
TEARDOWN F f_fix[f2]
TEARDOWN C c_fix
TEARDOWN C another_c_fix
TEARDOWN M m_fix[m2]
TEARDOWN P p_fix[p2]
TEARDOWN S s_fix
""".format(
test_fixtures_used
)
)
class TestAutouseManagement:
def test_autouse_conftest_mid_directory(self, testdir):
pkgdir = testdir.mkpydir("xyz123")

View File

@ -146,12 +146,16 @@ def test_is_generator_async_gen_syntax(testdir):
class ErrorsHelper:
@property
def raise_baseexception(self):
raise BaseException("base exception should be raised")
@property
def raise_exception(self):
raise Exception("exception should be catched")
@property
def raise_fail(self):
def raise_fail_outcome(self):
pytest.fail("fail should be catched")
@ -160,13 +164,15 @@ def test_helper_failures():
with pytest.raises(Exception):
helper.raise_exception
with pytest.raises(OutcomeException):
helper.raise_fail
helper.raise_fail_outcome
def test_safe_getattr():
helper = ErrorsHelper()
assert safe_getattr(helper, "raise_exception", "default") == "default"
assert safe_getattr(helper, "raise_fail", "default") == "default"
assert safe_getattr(helper, "raise_fail_outcome", "default") == "default"
with pytest.raises(BaseException):
assert safe_getattr(helper, "raise_baseexception", "default")
def test_safe_isclass():

View File

@ -821,8 +821,15 @@ def test_pass_reporting_on_fail(testdir):
def test_pass_output_reporting(testdir):
testdir.makepyfile(
"""
def setup_module():
print("setup_module")
def teardown_module():
print("teardown_module")
def test_pass_has_output():
print("Four score and seven years ago...")
def test_pass_no_output():
pass
"""
@ -837,8 +844,12 @@ def test_pass_output_reporting(testdir):
[
"*= PASSES =*",
"*_ test_pass_has_output _*",
"*- Captured stdout setup -*",
"setup_module",
"*- Captured stdout call -*",
"Four score and seven years ago...",
"*- Captured stdout teardown -*",
"teardown_module",
"*= short test summary info =*",
"PASSED test_pass_output_reporting.py::test_pass_has_output",
"PASSED test_pass_output_reporting.py::test_pass_no_output",

View File

@ -55,6 +55,10 @@ basepython = python3
deps = pre-commit>=1.11.0
commands = pre-commit run --all-files --show-diff-on-failure {posargs:}
[testenv:mypy]
extras = checkqa-mypy, testing
commands = mypy {posargs:src testing}
[testenv:docs]
basepython = python3
usedevelop = True
@ -131,7 +135,7 @@ commands = python scripts/release.py {posargs}
description = create GitHub release after deployment
basepython = python3
usedevelop = True
passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG TRAVIS_REPO_SLUG
passenv = GH_RELEASE_NOTES_TOKEN GITHUB_REF GITHUB_REPOSITORY
deps =
github3.py
pypandoc