Merge master into features

Conflicts:
	.github/workflows/main.yml
This commit is contained in:
Daniel Hahler
2020-01-16 19:45:52 +01:00
27 changed files with 316 additions and 37 deletions

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

@@ -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)