Format docstrings in a consistent style
This commit is contained in:
parent
6882c0368b
commit
0242de4f56
|
@ -182,7 +182,7 @@ Mark a test function as using the given fixture names.
|
||||||
|
|
||||||
.. py:function:: pytest.mark.usefixtures(*names)
|
.. py:function:: pytest.mark.usefixtures(*names)
|
||||||
|
|
||||||
:param args: the names of the fixture to use, as strings
|
:param args: The names of the fixture to use, as strings.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -209,8 +209,10 @@ Marks a test function as *expected to fail*.
|
||||||
Condition for marking the test function as xfail (``True/False`` or a
|
Condition for marking the test function as xfail (``True/False`` or a
|
||||||
:ref:`condition string <string conditions>`). If a bool, you also have
|
:ref:`condition string <string conditions>`). If a bool, you also have
|
||||||
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
to specify ``reason`` (see :ref:`condition string <string conditions>`).
|
||||||
:keyword str reason: Reason why the test function is marked as xfail.
|
:keyword str reason:
|
||||||
:keyword Exception raises: Exception subclass expected to be raised by the test function; other exceptions will fail the test.
|
Reason why the test function is marked as xfail.
|
||||||
|
:keyword Type[Exception] raises:
|
||||||
|
Exception subclass expected to be raised by the test function; other exceptions will fail the test.
|
||||||
:keyword bool run:
|
:keyword bool run:
|
||||||
If the test function should actually be executed. If ``False``, the function will always xfail and will
|
If the test function should actually be executed. If ``False``, the function will always xfail and will
|
||||||
not be executed (useful if a function is segfaulting).
|
not be executed (useful if a function is segfaulting).
|
||||||
|
@ -224,7 +226,7 @@ Marks a test function as *expected to fail*.
|
||||||
a new release of a library fixes a known bug).
|
a new release of a library fixes a known bug).
|
||||||
|
|
||||||
|
|
||||||
custom marks
|
Custom marks
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
Marks are created dynamically using the factory object ``pytest.mark`` and applied as a decorator.
|
Marks are created dynamically using the factory object ``pytest.mark`` and applied as a decorator.
|
||||||
|
@ -473,7 +475,7 @@ caplog
|
||||||
.. autofunction:: _pytest.logging.caplog()
|
.. autofunction:: _pytest.logging.caplog()
|
||||||
:no-auto-options:
|
:no-auto-options:
|
||||||
|
|
||||||
This returns a :class:`_pytest.logging.LogCaptureFixture` instance.
|
Returns a :class:`_pytest.logging.LogCaptureFixture` instance.
|
||||||
|
|
||||||
.. autoclass:: _pytest.logging.LogCaptureFixture
|
.. autoclass:: _pytest.logging.LogCaptureFixture
|
||||||
:members:
|
:members:
|
||||||
|
@ -491,7 +493,7 @@ monkeypatch
|
||||||
.. autofunction:: _pytest.monkeypatch.monkeypatch()
|
.. autofunction:: _pytest.monkeypatch.monkeypatch()
|
||||||
:no-auto-options:
|
:no-auto-options:
|
||||||
|
|
||||||
This returns a :class:`MonkeyPatch` instance.
|
Returns a :class:`MonkeyPatch` instance.
|
||||||
|
|
||||||
.. autoclass:: _pytest.monkeypatch.MonkeyPatch
|
.. autoclass:: _pytest.monkeypatch.MonkeyPatch
|
||||||
:members:
|
:members:
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
"""allow bash-completion for argparse with argcomplete if installed
|
"""Allow bash-completion for argparse with argcomplete if installed.
|
||||||
needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
|
|
||||||
|
Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
|
||||||
to find the magic string, so _ARGCOMPLETE env. var is never set, and
|
to find the magic string, so _ARGCOMPLETE env. var is never set, and
|
||||||
this does not need special code.
|
this does not need special code).
|
||||||
|
|
||||||
Function try_argcomplete(parser) should be called directly before
|
Function try_argcomplete(parser) should be called directly before
|
||||||
the call to ArgumentParser.parse_args().
|
the call to ArgumentParser.parse_args().
|
||||||
|
@ -10,8 +11,7 @@ The filescompleter is what you normally would use on the positional
|
||||||
arguments specification, in order to get "dirname/" after "dirn<TAB>"
|
arguments specification, in order to get "dirname/" after "dirn<TAB>"
|
||||||
instead of the default "dirname ":
|
instead of the default "dirname ":
|
||||||
|
|
||||||
optparser.add_argument(Config._file_or_dir, nargs='*'
|
optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
|
||||||
).completer=filescompleter
|
|
||||||
|
|
||||||
Other, application specific, completers should go in the file
|
Other, application specific, completers should go in the file
|
||||||
doing the add_argument calls as they need to be specified as .completer
|
doing the add_argument calls as they need to be specified as .completer
|
||||||
|
@ -20,35 +20,43 @@ attribute points to will not be used).
|
||||||
|
|
||||||
SPEEDUP
|
SPEEDUP
|
||||||
=======
|
=======
|
||||||
|
|
||||||
The generic argcomplete script for bash-completion
|
The generic argcomplete script for bash-completion
|
||||||
(/etc/bash_completion.d/python-argcomplete.sh )
|
(/etc/bash_completion.d/python-argcomplete.sh)
|
||||||
uses a python program to determine startup script generated by pip.
|
uses a python program to determine startup script generated by pip.
|
||||||
You can speed up completion somewhat by changing this script to include
|
You can speed up completion somewhat by changing this script to include
|
||||||
# PYTHON_ARGCOMPLETE_OK
|
# PYTHON_ARGCOMPLETE_OK
|
||||||
so the the python-argcomplete-check-easy-install-script does not
|
so the the python-argcomplete-check-easy-install-script does not
|
||||||
need to be called to find the entry point of the code and see if that is
|
need to be called to find the entry point of the code and see if that is
|
||||||
marked with PYTHON_ARGCOMPLETE_OK
|
marked with PYTHON_ARGCOMPLETE_OK.
|
||||||
|
|
||||||
INSTALL/DEBUGGING
|
INSTALL/DEBUGGING
|
||||||
=================
|
=================
|
||||||
|
|
||||||
To include this support in another application that has setup.py generated
|
To include this support in another application that has setup.py generated
|
||||||
scripts:
|
scripts:
|
||||||
- add the line:
|
|
||||||
|
- Add the line:
|
||||||
# PYTHON_ARGCOMPLETE_OK
|
# PYTHON_ARGCOMPLETE_OK
|
||||||
near the top of the main python entry point
|
near the top of the main python entry point.
|
||||||
- include in the file calling parse_args():
|
|
||||||
|
- Include in the file calling parse_args():
|
||||||
from _argcomplete import try_argcomplete, filescompleter
|
from _argcomplete import try_argcomplete, filescompleter
|
||||||
, call try_argcomplete just before parse_args(), and optionally add
|
Call try_argcomplete just before parse_args(), and optionally add
|
||||||
filescompleter to the positional arguments' add_argument()
|
filescompleter to the positional arguments' add_argument().
|
||||||
|
|
||||||
If things do not work right away:
|
If things do not work right away:
|
||||||
- switch on argcomplete debugging with (also helpful when doing custom
|
|
||||||
|
- Switch on argcomplete debugging with (also helpful when doing custom
|
||||||
completers):
|
completers):
|
||||||
export _ARC_DEBUG=1
|
export _ARC_DEBUG=1
|
||||||
- run:
|
|
||||||
|
- Run:
|
||||||
python-argcomplete-check-easy-install-script $(which appname)
|
python-argcomplete-check-easy-install-script $(which appname)
|
||||||
echo $?
|
echo $?
|
||||||
will echo 0 if the magic line has been found, 1 if not
|
will echo 0 if the magic line has been found, 1 if not.
|
||||||
- sometimes it helps to find early on errors using:
|
|
||||||
|
- Sometimes it helps to find early on errors using:
|
||||||
_ARGCOMPLETE=1 _ARC_DEBUG=1 appname
|
_ARGCOMPLETE=1 _ARC_DEBUG=1 appname
|
||||||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||||
global argcomplete script).
|
global argcomplete script).
|
||||||
|
@ -63,13 +71,13 @@ from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class FastFilesCompleter:
|
class FastFilesCompleter:
|
||||||
"Fast file completer class"
|
"""Fast file completer class."""
|
||||||
|
|
||||||
def __init__(self, directories: bool = True) -> None:
|
def __init__(self, directories: bool = True) -> None:
|
||||||
self.directories = directories
|
self.directories = directories
|
||||||
|
|
||||||
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
||||||
"""only called on non option completions"""
|
# Only called on non option completions.
|
||||||
if os.path.sep in prefix[1:]:
|
if os.path.sep in prefix[1:]:
|
||||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||||
else:
|
else:
|
||||||
|
@ -77,7 +85,7 @@ class FastFilesCompleter:
|
||||||
completion = []
|
completion = []
|
||||||
globbed = []
|
globbed = []
|
||||||
if "*" not in prefix and "?" not in prefix:
|
if "*" not in prefix and "?" not in prefix:
|
||||||
# we are on unix, otherwise no bash
|
# We are on unix, otherwise no bash.
|
||||||
if not prefix or prefix[-1] == os.path.sep:
|
if not prefix or prefix[-1] == os.path.sep:
|
||||||
globbed.extend(glob(prefix + ".*"))
|
globbed.extend(glob(prefix + ".*"))
|
||||||
prefix += "*"
|
prefix += "*"
|
||||||
|
@ -85,7 +93,7 @@ class FastFilesCompleter:
|
||||||
for x in sorted(globbed):
|
for x in sorted(globbed):
|
||||||
if os.path.isdir(x):
|
if os.path.isdir(x):
|
||||||
x += "/"
|
x += "/"
|
||||||
# append stripping the prefix (like bash, not like compgen)
|
# Append stripping the prefix (like bash, not like compgen).
|
||||||
completion.append(x[prefix_dir:])
|
completion.append(x[prefix_dir:])
|
||||||
return completion
|
return completion
|
||||||
|
|
||||||
|
|
|
@ -71,9 +71,8 @@ class Code:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self) -> Union[py.path.local, str]:
|
def path(self) -> Union[py.path.local, str]:
|
||||||
"""Return a path object pointing to source code (or a str in case
|
"""Return a path object pointing to source code, or an ``str`` in
|
||||||
of OSError / non-existing file).
|
case of ``OSError`` / non-existing file."""
|
||||||
"""
|
|
||||||
if not self.raw.co_filename:
|
if not self.raw.co_filename:
|
||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
|
@ -420,15 +419,16 @@ class ExceptionInfo(Generic[_E]):
|
||||||
exc_info: Tuple["Type[_E]", "_E", TracebackType],
|
exc_info: Tuple["Type[_E]", "_E", TracebackType],
|
||||||
exprinfo: Optional[str] = None,
|
exprinfo: Optional[str] = None,
|
||||||
) -> "ExceptionInfo[_E]":
|
) -> "ExceptionInfo[_E]":
|
||||||
"""Returns an ExceptionInfo for an existing exc_info tuple.
|
"""Return an ExceptionInfo for an existing exc_info tuple.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
Experimental API
|
Experimental API
|
||||||
|
|
||||||
:param exprinfo: a text string helping to determine if we should
|
:param exprinfo:
|
||||||
strip ``AssertionError`` from the output, defaults
|
A text string helping to determine if we should strip
|
||||||
to the exception message/``__str__()``
|
``AssertionError`` from the output. Defaults to the exception
|
||||||
|
message/``__str__()``.
|
||||||
"""
|
"""
|
||||||
_striptext = ""
|
_striptext = ""
|
||||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||||
|
@ -444,15 +444,16 @@ class ExceptionInfo(Generic[_E]):
|
||||||
def from_current(
|
def from_current(
|
||||||
cls, exprinfo: Optional[str] = None
|
cls, exprinfo: Optional[str] = None
|
||||||
) -> "ExceptionInfo[BaseException]":
|
) -> "ExceptionInfo[BaseException]":
|
||||||
"""Returns an ExceptionInfo matching the current traceback.
|
"""Return an ExceptionInfo matching the current traceback.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
Experimental API
|
Experimental API
|
||||||
|
|
||||||
:param exprinfo: a text string helping to determine if we should
|
:param exprinfo:
|
||||||
strip ``AssertionError`` from the output, defaults
|
A text string helping to determine if we should strip
|
||||||
to the exception message/``__str__()``
|
``AssertionError`` from the output. Defaults to the exception
|
||||||
|
message/``__str__()``.
|
||||||
"""
|
"""
|
||||||
tup = sys.exc_info()
|
tup = sys.exc_info()
|
||||||
assert tup[0] is not None, "no current exception"
|
assert tup[0] is not None, "no current exception"
|
||||||
|
@ -467,7 +468,7 @@ class ExceptionInfo(Generic[_E]):
|
||||||
return cls(None)
|
return cls(None)
|
||||||
|
|
||||||
def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
|
def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None:
|
||||||
"""fill an unfilled ExceptionInfo created with for_later()"""
|
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
|
||||||
assert self._excinfo is None, "ExceptionInfo was already filled"
|
assert self._excinfo is None, "ExceptionInfo was already filled"
|
||||||
self._excinfo = exc_info
|
self._excinfo = exc_info
|
||||||
|
|
||||||
|
@ -568,7 +569,8 @@ class ExceptionInfo(Generic[_E]):
|
||||||
Show locals per traceback entry.
|
Show locals per traceback entry.
|
||||||
Ignored if ``style=="native"``.
|
Ignored if ``style=="native"``.
|
||||||
|
|
||||||
:param str style: long|short|no|native|value traceback style
|
:param str style:
|
||||||
|
long|short|no|native|value traceback style.
|
||||||
|
|
||||||
:param bool abspath:
|
:param bool abspath:
|
||||||
If paths should be changed to absolute or left unchanged.
|
If paths should be changed to absolute or left unchanged.
|
||||||
|
@ -583,7 +585,8 @@ class ExceptionInfo(Generic[_E]):
|
||||||
:param bool truncate_locals:
|
:param bool truncate_locals:
|
||||||
With ``showlocals==True``, make sure locals can be safely represented as strings.
|
With ``showlocals==True``, make sure locals can be safely represented as strings.
|
||||||
|
|
||||||
:param bool chain: if chained exceptions in Python 3 should be shown.
|
:param bool chain:
|
||||||
|
If chained exceptions in Python 3 should be shown.
|
||||||
|
|
||||||
.. versionchanged:: 3.9
|
.. versionchanged:: 3.9
|
||||||
|
|
||||||
|
@ -643,7 +646,7 @@ class FormattedExcinfo:
|
||||||
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
||||||
|
|
||||||
def _getindent(self, source: "Source") -> int:
|
def _getindent(self, source: "Source") -> int:
|
||||||
# figure out indent for given source
|
# Figure out indent for the given source.
|
||||||
try:
|
try:
|
||||||
s = str(source.getstatement(len(source) - 1))
|
s = str(source.getstatement(len(source) - 1))
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
@ -704,7 +707,7 @@ class FormattedExcinfo:
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
lines = []
|
lines = []
|
||||||
indentstr = " " * indent
|
indentstr = " " * indent
|
||||||
# get the real exception information out
|
# Get the real exception information out.
|
||||||
exlines = excinfo.exconly(tryshort=True).split("\n")
|
exlines = excinfo.exconly(tryshort=True).split("\n")
|
||||||
failindent = self.fail_marker + indentstr[1:]
|
failindent = self.fail_marker + indentstr[1:]
|
||||||
for line in exlines:
|
for line in exlines:
|
||||||
|
@ -730,8 +733,7 @@ class FormattedExcinfo:
|
||||||
str_repr = saferepr(value)
|
str_repr = saferepr(value)
|
||||||
else:
|
else:
|
||||||
str_repr = safeformat(value)
|
str_repr = safeformat(value)
|
||||||
# if len(str_repr) < 70 or not isinstance(value,
|
# if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
|
||||||
# (list, tuple, dict)):
|
|
||||||
lines.append("{:<10} = {}".format(name, str_repr))
|
lines.append("{:<10} = {}".format(name, str_repr))
|
||||||
# else:
|
# else:
|
||||||
# self._line("%-10s =\\" % (name,))
|
# self._line("%-10s =\\" % (name,))
|
||||||
|
@ -809,16 +811,17 @@ class FormattedExcinfo:
|
||||||
def _truncate_recursive_traceback(
|
def _truncate_recursive_traceback(
|
||||||
self, traceback: Traceback
|
self, traceback: Traceback
|
||||||
) -> Tuple[Traceback, Optional[str]]:
|
) -> Tuple[Traceback, Optional[str]]:
|
||||||
"""
|
"""Truncate the given recursive traceback trying to find the starting
|
||||||
Truncate the given recursive traceback trying to find the starting point
|
point of the recursion.
|
||||||
of the recursion.
|
|
||||||
|
|
||||||
The detection is done by going through each traceback entry and finding the
|
The detection is done by going through each traceback entry and
|
||||||
point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
|
finding the point in which the locals of the frame are equal to the
|
||||||
|
locals of a previous frame (see ``recursionindex()``).
|
||||||
|
|
||||||
Handle the situation where the recursion process might raise an exception (for example
|
Handle the situation where the recursion process might raise an
|
||||||
comparing numpy arrays using equality raises a TypeError), in which case we do our best to
|
exception (for example comparing numpy arrays using equality raises a
|
||||||
warn the user of the error and show a limited traceback.
|
TypeError), in which case we do our best to warn the user of the
|
||||||
|
error and show a limited traceback.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
recursionindex = traceback.recursionindex()
|
recursionindex = traceback.recursionindex()
|
||||||
|
@ -863,8 +866,8 @@ class FormattedExcinfo:
|
||||||
excinfo_._getreprcrash() if self.style != "value" else None
|
excinfo_._getreprcrash() if self.style != "value" else None
|
||||||
) # type: Optional[ReprFileLocation]
|
) # type: Optional[ReprFileLocation]
|
||||||
else:
|
else:
|
||||||
# fallback to native repr if the exception doesn't have a traceback:
|
# Fallback to native repr if the exception doesn't have a traceback:
|
||||||
# ExceptionInfo objects require a full traceback to work
|
# ExceptionInfo objects require a full traceback to work.
|
||||||
reprtraceback = ReprTracebackNative(
|
reprtraceback = ReprTracebackNative(
|
||||||
traceback.format_exception(type(e), e, None)
|
traceback.format_exception(type(e), e, None)
|
||||||
)
|
)
|
||||||
|
@ -915,7 +918,7 @@ class TerminalRepr:
|
||||||
# This class is abstract -- only subclasses are instantiated.
|
# This class is abstract -- only subclasses are instantiated.
|
||||||
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
@attr.s(**{ATTRS_EQ_FIELD: False}) # type: ignore
|
||||||
class ExceptionRepr(TerminalRepr):
|
class ExceptionRepr(TerminalRepr):
|
||||||
# Provided by in subclasses.
|
# Provided by subclasses.
|
||||||
reprcrash = None # type: Optional[ReprFileLocation]
|
reprcrash = None # type: Optional[ReprFileLocation]
|
||||||
reprtraceback = None # type: ReprTraceback
|
reprtraceback = None # type: ReprTraceback
|
||||||
|
|
||||||
|
@ -942,7 +945,7 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||||
def __attrs_post_init__(self) -> None:
|
def __attrs_post_init__(self) -> None:
|
||||||
super().__attrs_post_init__()
|
super().__attrs_post_init__()
|
||||||
# reprcrash and reprtraceback of the outermost (the newest) exception
|
# reprcrash and reprtraceback of the outermost (the newest) exception
|
||||||
# in the chain
|
# in the chain.
|
||||||
self.reprtraceback = self.chain[-1][0]
|
self.reprtraceback = self.chain[-1][0]
|
||||||
self.reprcrash = self.chain[-1][1]
|
self.reprcrash = self.chain[-1][1]
|
||||||
|
|
||||||
|
@ -974,7 +977,7 @@ class ReprTraceback(TerminalRepr):
|
||||||
entrysep = "_ "
|
entrysep = "_ "
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
# the entries might have different styles
|
# The entries might have different styles.
|
||||||
for i, entry in enumerate(self.reprentries):
|
for i, entry in enumerate(self.reprentries):
|
||||||
if entry.style == "long":
|
if entry.style == "long":
|
||||||
tw.line("")
|
tw.line("")
|
||||||
|
@ -1017,7 +1020,7 @@ class ReprEntry(TerminalRepr):
|
||||||
style = attr.ib(type="_TracebackStyle")
|
style = attr.ib(type="_TracebackStyle")
|
||||||
|
|
||||||
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
||||||
"""Writes the source code portions of a list of traceback entries with syntax highlighting.
|
"""Write the source code portions of a list of traceback entries with syntax highlighting.
|
||||||
|
|
||||||
Usually entries are lines like these:
|
Usually entries are lines like these:
|
||||||
|
|
||||||
|
@ -1099,8 +1102,8 @@ class ReprFileLocation(TerminalRepr):
|
||||||
message = attr.ib(type=str)
|
message = attr.ib(type=str)
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
# filename and lineno output for each entry,
|
# Filename and lineno output for each entry, using an output format
|
||||||
# using an output format that most editors understand
|
# that most editors understand.
|
||||||
msg = self.message
|
msg = self.message
|
||||||
i = msg.find("\n")
|
i = msg.find("\n")
|
||||||
if i != -1:
|
if i != -1:
|
||||||
|
@ -1175,10 +1178,10 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]:
|
||||||
return code.path, code.firstlineno
|
return code.path, code.firstlineno
|
||||||
|
|
||||||
|
|
||||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
# Relative paths that we use to filter traceback entries from appearing to the user;
|
||||||
# see filter_traceback
|
# see filter_traceback.
|
||||||
# note: if we need to add more paths than what we have now we should probably use a list
|
# note: if we need to add more paths than what we have now we should probably use a list
|
||||||
# for better maintenance
|
# for better maintenance.
|
||||||
|
|
||||||
_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc"))
|
_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc"))
|
||||||
# pluggy is either a package or a single module depending on the version
|
# pluggy is either a package or a single module depending on the version
|
||||||
|
@ -1197,14 +1200,14 @@ def filter_traceback(entry: TracebackEntry) -> bool:
|
||||||
* internal traceback from pytest or its internal libraries, py and pluggy.
|
* internal traceback from pytest or its internal libraries, py and pluggy.
|
||||||
"""
|
"""
|
||||||
# entry.path might sometimes return a str object when the entry
|
# entry.path might sometimes return a str object when the entry
|
||||||
# points to dynamically generated code
|
# points to dynamically generated code.
|
||||||
# see https://bitbucket.org/pytest-dev/py/issues/71
|
# See https://bitbucket.org/pytest-dev/py/issues/71.
|
||||||
raw_filename = entry.frame.code.raw.co_filename
|
raw_filename = entry.frame.code.raw.co_filename
|
||||||
is_generated = "<" in raw_filename and ">" in raw_filename
|
is_generated = "<" in raw_filename and ">" in raw_filename
|
||||||
if is_generated:
|
if is_generated:
|
||||||
return False
|
return False
|
||||||
# entry.path might point to a non-existing file, in which case it will
|
# entry.path might point to a non-existing file, in which case it will
|
||||||
# also return a str object. see #1133
|
# also return a str object. See #1133.
|
||||||
p = py.path.local(entry.path)
|
p = py.path.local(entry.path)
|
||||||
return (
|
return (
|
||||||
not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR)
|
not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR)
|
||||||
|
|
|
@ -67,9 +67,7 @@ class Source:
|
||||||
return len(self.lines)
|
return len(self.lines)
|
||||||
|
|
||||||
def strip(self) -> "Source":
|
def strip(self) -> "Source":
|
||||||
""" return new source object with trailing
|
"""Return new Source object with trailing and leading blank lines removed."""
|
||||||
and leading blank lines removed.
|
|
||||||
"""
|
|
||||||
start, end = 0, len(self)
|
start, end = 0, len(self)
|
||||||
while start < end and not self.lines[start].strip():
|
while start < end and not self.lines[start].strip():
|
||||||
start += 1
|
start += 1
|
||||||
|
@ -80,31 +78,28 @@ class Source:
|
||||||
return source
|
return source
|
||||||
|
|
||||||
def indent(self, indent: str = " " * 4) -> "Source":
|
def indent(self, indent: str = " " * 4) -> "Source":
|
||||||
""" return a copy of the source object with
|
"""Return a copy of the source object with all lines indented by the
|
||||||
all lines indented by the given indent-string.
|
given indent-string."""
|
||||||
"""
|
|
||||||
newsource = Source()
|
newsource = Source()
|
||||||
newsource.lines = [(indent + line) for line in self.lines]
|
newsource.lines = [(indent + line) for line in self.lines]
|
||||||
return newsource
|
return newsource
|
||||||
|
|
||||||
def getstatement(self, lineno: int) -> "Source":
|
def getstatement(self, lineno: int) -> "Source":
|
||||||
""" return Source statement which contains the
|
"""Return Source statement which contains the given linenumber
|
||||||
given linenumber (counted from 0).
|
(counted from 0)."""
|
||||||
"""
|
|
||||||
start, end = self.getstatementrange(lineno)
|
start, end = self.getstatementrange(lineno)
|
||||||
return self[start:end]
|
return self[start:end]
|
||||||
|
|
||||||
def getstatementrange(self, lineno: int) -> Tuple[int, int]:
|
def getstatementrange(self, lineno: int) -> Tuple[int, int]:
|
||||||
""" return (start, end) tuple which spans the minimal
|
"""Return (start, end) tuple which spans the minimal statement region
|
||||||
statement region which containing the given lineno.
|
which containing the given lineno."""
|
||||||
"""
|
|
||||||
if not (0 <= lineno < len(self)):
|
if not (0 <= lineno < len(self)):
|
||||||
raise IndexError("lineno out of range")
|
raise IndexError("lineno out of range")
|
||||||
ast, start, end = getstatementrange_ast(lineno, self)
|
ast, start, end = getstatementrange_ast(lineno, self)
|
||||||
return start, end
|
return start, end
|
||||||
|
|
||||||
def deindent(self) -> "Source":
|
def deindent(self) -> "Source":
|
||||||
"""return a new source object deindented."""
|
"""Return a new Source object deindented."""
|
||||||
newsource = Source()
|
newsource = Source()
|
||||||
newsource.lines[:] = deindent(self.lines)
|
newsource.lines[:] = deindent(self.lines)
|
||||||
return newsource
|
return newsource
|
||||||
|
@ -129,7 +124,7 @@ def findsource(obj) -> Tuple[Optional[Source], int]:
|
||||||
|
|
||||||
|
|
||||||
def getrawcode(obj, trycall: bool = True):
|
def getrawcode(obj, trycall: bool = True):
|
||||||
""" return code object for given function. """
|
"""Return code object for given function."""
|
||||||
try:
|
try:
|
||||||
return obj.__code__
|
return obj.__code__
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -148,8 +143,8 @@ def deindent(lines: Iterable[str]) -> List[str]:
|
||||||
|
|
||||||
|
|
||||||
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
|
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
|
||||||
# flatten all statements and except handlers into one lineno-list
|
# Flatten all statements and except handlers into one lineno-list.
|
||||||
# AST's line numbers start indexing at 1
|
# AST's line numbers start indexing at 1.
|
||||||
values = [] # type: List[int]
|
values = [] # type: List[int]
|
||||||
for x in ast.walk(node):
|
for x in ast.walk(node):
|
||||||
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
||||||
|
@ -157,7 +152,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
|
||||||
for name in ("finalbody", "orelse"):
|
for name in ("finalbody", "orelse"):
|
||||||
val = getattr(x, name, None) # type: Optional[List[ast.stmt]]
|
val = getattr(x, name, None) # type: Optional[List[ast.stmt]]
|
||||||
if val:
|
if val:
|
||||||
# treat the finally/orelse part as its own statement
|
# Treat the finally/orelse part as its own statement.
|
||||||
values.append(val[0].lineno - 1 - 1)
|
values.append(val[0].lineno - 1 - 1)
|
||||||
values.sort()
|
values.sort()
|
||||||
insert_index = bisect_right(values, lineno)
|
insert_index = bisect_right(values, lineno)
|
||||||
|
@ -178,13 +173,13 @@ def getstatementrange_ast(
|
||||||
if astnode is None:
|
if astnode is None:
|
||||||
content = str(source)
|
content = str(source)
|
||||||
# See #4260:
|
# See #4260:
|
||||||
# don't produce duplicate warnings when compiling source to find ast
|
# Don't produce duplicate warnings when compiling source to find AST.
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
astnode = ast.parse(content, "source", "exec")
|
astnode = ast.parse(content, "source", "exec")
|
||||||
|
|
||||||
start, end = get_statement_startend2(lineno, astnode)
|
start, end = get_statement_startend2(lineno, astnode)
|
||||||
# we need to correct the end:
|
# We need to correct the end:
|
||||||
# - ast-parsing strips comments
|
# - ast-parsing strips comments
|
||||||
# - there might be empty lines
|
# - there might be empty lines
|
||||||
# - we might have lesser indented code blocks at the end
|
# - we might have lesser indented code blocks at the end
|
||||||
|
@ -192,10 +187,10 @@ def getstatementrange_ast(
|
||||||
end = len(source.lines)
|
end = len(source.lines)
|
||||||
|
|
||||||
if end > start + 1:
|
if end > start + 1:
|
||||||
# make sure we don't span differently indented code blocks
|
# Make sure we don't span differently indented code blocks
|
||||||
# by using the BlockFinder helper used which inspect.getsource() uses itself
|
# by using the BlockFinder helper used which inspect.getsource() uses itself.
|
||||||
block_finder = inspect.BlockFinder()
|
block_finder = inspect.BlockFinder()
|
||||||
# if we start with an indented line, put blockfinder to "started" mode
|
# If we start with an indented line, put blockfinder to "started" mode.
|
||||||
block_finder.started = source.lines[start][0].isspace()
|
block_finder.started = source.lines[start][0].isspace()
|
||||||
it = ((x + "\n") for x in source.lines[start:end])
|
it = ((x + "\n") for x in source.lines[start:end])
|
||||||
try:
|
try:
|
||||||
|
@ -206,7 +201,7 @@ def getstatementrange_ast(
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# the end might still point to a comment or empty line, correct it
|
# The end might still point to a comment or empty line, correct it.
|
||||||
while end:
|
while end:
|
||||||
line = source.lines[end - 1].lstrip()
|
line = source.lines[end - 1].lstrip()
|
||||||
if line.startswith("#") or not line:
|
if line.startswith("#") or not line:
|
||||||
|
|
|
@ -36,9 +36,8 @@ def _ellipsize(s: str, maxsize: int) -> str:
|
||||||
|
|
||||||
|
|
||||||
class SafeRepr(reprlib.Repr):
|
class SafeRepr(reprlib.Repr):
|
||||||
"""subclass of repr.Repr that limits the resulting size of repr()
|
"""repr.Repr that limits the resulting size of repr() and includes
|
||||||
and includes information on exceptions raised during the call.
|
information on exceptions raised during the call."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, maxsize: int) -> None:
|
def __init__(self, maxsize: int) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
@ -65,7 +64,8 @@ class SafeRepr(reprlib.Repr):
|
||||||
|
|
||||||
|
|
||||||
def safeformat(obj: object) -> str:
|
def safeformat(obj: object) -> str:
|
||||||
"""return a pretty printed string for the given object.
|
"""Return a pretty printed string for the given object.
|
||||||
|
|
||||||
Failing __repr__ functions of user instances will be represented
|
Failing __repr__ functions of user instances will be represented
|
||||||
with a short exception info.
|
with a short exception info.
|
||||||
"""
|
"""
|
||||||
|
@ -76,11 +76,14 @@ def safeformat(obj: object) -> str:
|
||||||
|
|
||||||
|
|
||||||
def saferepr(obj: object, maxsize: int = 240) -> str:
|
def saferepr(obj: object, maxsize: int = 240) -> str:
|
||||||
"""return a size-limited safe repr-string for the given object.
|
"""Return a size-limited safe repr-string for the given object.
|
||||||
|
|
||||||
Failing __repr__ functions of user instances will be represented
|
Failing __repr__ functions of user instances will be represented
|
||||||
with a short exception info and 'saferepr' generally takes
|
with a short exception info and 'saferepr' generally takes
|
||||||
care to never raise exceptions itself. This function is a wrapper
|
care to never raise exceptions itself.
|
||||||
around the Repr/reprlib functionality of the standard 2.6 lib.
|
|
||||||
|
This function is a wrapper around the Repr/reprlib functionality of the
|
||||||
|
standard 2.6 lib.
|
||||||
"""
|
"""
|
||||||
return SafeRepr(maxsize).repr(obj)
|
return SafeRepr(maxsize).repr(obj)
|
||||||
|
|
||||||
|
|
|
@ -111,13 +111,13 @@ class TerminalWriter:
|
||||||
) -> None:
|
) -> None:
|
||||||
if fullwidth is None:
|
if fullwidth is None:
|
||||||
fullwidth = self.fullwidth
|
fullwidth = self.fullwidth
|
||||||
# the goal is to have the line be as long as possible
|
# The goal is to have the line be as long as possible
|
||||||
# under the condition that len(line) <= fullwidth
|
# under the condition that len(line) <= fullwidth.
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
# if we print in the last column on windows we are on a
|
# If we print in the last column on windows we are on a
|
||||||
# new line but there is no way to verify/neutralize this
|
# new line but there is no way to verify/neutralize this
|
||||||
# (we may not know the exact line width)
|
# (we may not know the exact line width).
|
||||||
# so let's be defensive to avoid empty lines in the output
|
# So let's be defensive to avoid empty lines in the output.
|
||||||
fullwidth -= 1
|
fullwidth -= 1
|
||||||
if title is not None:
|
if title is not None:
|
||||||
# we want 2 + 2*len(fill) + len(title) <= fullwidth
|
# we want 2 + 2*len(fill) + len(title) <= fullwidth
|
||||||
|
@ -131,9 +131,9 @@ class TerminalWriter:
|
||||||
# we want len(sepchar)*N <= fullwidth
|
# we want len(sepchar)*N <= fullwidth
|
||||||
# i.e. N <= fullwidth // len(sepchar)
|
# i.e. N <= fullwidth // len(sepchar)
|
||||||
line = sepchar * (fullwidth // len(sepchar))
|
line = sepchar * (fullwidth // len(sepchar))
|
||||||
# in some situations there is room for an extra sepchar at the right,
|
# In some situations there is room for an extra sepchar at the right,
|
||||||
# in particular if we consider that with a sepchar like "_ " the
|
# in particular if we consider that with a sepchar like "_ " the
|
||||||
# trailing space is not important at the end of the line
|
# trailing space is not important at the end of the line.
|
||||||
if len(line) + len(sepchar.rstrip()) <= fullwidth:
|
if len(line) + len(sepchar.rstrip()) <= fullwidth:
|
||||||
line += sepchar.rstrip()
|
line += sepchar.rstrip()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
"""
|
"""Support for presenting detailed information in failing assertions."""
|
||||||
support for presenting detailed information in failing assertions.
|
|
||||||
"""
|
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
@ -55,7 +53,7 @@ def register_assert_rewrite(*names: str) -> None:
|
||||||
actually imported, usually in your __init__.py if you are a plugin
|
actually imported, usually in your __init__.py if you are a plugin
|
||||||
using a package.
|
using a package.
|
||||||
|
|
||||||
:raise TypeError: if the given module names are not strings.
|
:raises TypeError: If the given module names are not strings.
|
||||||
"""
|
"""
|
||||||
for name in names:
|
for name in names:
|
||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
|
@ -105,9 +103,9 @@ def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
|
||||||
|
|
||||||
|
|
||||||
def pytest_collection(session: "Session") -> None:
|
def pytest_collection(session: "Session") -> None:
|
||||||
# this hook is only called when test modules are collected
|
# This hook is only called when test modules are collected
|
||||||
# so for example not in the master process of pytest-xdist
|
# so for example not in the master process of pytest-xdist
|
||||||
# (which does not collect test modules)
|
# (which does not collect test modules).
|
||||||
assertstate = session.config._store.get(assertstate_key, None)
|
assertstate = session.config._store.get(assertstate_key, None)
|
||||||
if assertstate:
|
if assertstate:
|
||||||
if assertstate.hook is not None:
|
if assertstate.hook is not None:
|
||||||
|
@ -116,18 +114,17 @@ def pytest_collection(session: "Session") -> None:
|
||||||
|
|
||||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||||
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks
|
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
|
||||||
|
|
||||||
The rewrite module will use util._reprcompare if
|
The rewrite module will use util._reprcompare if it exists to use custom
|
||||||
it exists to use custom reporting via the
|
reporting via the pytest_assertrepr_compare hook. This sets up this custom
|
||||||
pytest_assertrepr_compare hook. This sets up this custom
|
|
||||||
comparison for the test.
|
comparison for the test.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ihook = item.ihook
|
ihook = item.ihook
|
||||||
|
|
||||||
def callbinrepr(op, left: object, right: object) -> Optional[str]:
|
def callbinrepr(op, left: object, right: object) -> Optional[str]:
|
||||||
"""Call the pytest_assertrepr_compare hook and prepare the result
|
"""Call the pytest_assertrepr_compare hook and prepare the result.
|
||||||
|
|
||||||
This uses the first result from the hook and then ensures the
|
This uses the first result from the hook and then ensures the
|
||||||
following:
|
following:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Rewrite assertion AST to produce nice error messages"""
|
"""Rewrite assertion AST to produce nice error messages."""
|
||||||
import ast
|
import ast
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
|
@ -170,7 +170,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||||
exec(co, module.__dict__)
|
exec(co, module.__dict__)
|
||||||
|
|
||||||
def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
|
def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
|
||||||
"""This is a fast way to get out of rewriting modules.
|
"""A fast way to get out of rewriting modules.
|
||||||
|
|
||||||
Profiling has shown that the call to PathFinder.find_spec (inside of
|
Profiling has shown that the call to PathFinder.find_spec (inside of
|
||||||
the find_spec from this class) is a major slowdown, so, this method
|
the find_spec from this class) is a major slowdown, so, this method
|
||||||
|
@ -350,7 +350,7 @@ else:
|
||||||
|
|
||||||
|
|
||||||
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
|
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
|
||||||
"""read and rewrite *fn* and return the code object."""
|
"""Read and rewrite *fn* and return the code object."""
|
||||||
fn_ = fspath(fn)
|
fn_ = fspath(fn)
|
||||||
stat = os.stat(fn_)
|
stat = os.stat(fn_)
|
||||||
with open(fn_, "rb") as f:
|
with open(fn_, "rb") as f:
|
||||||
|
@ -411,7 +411,7 @@ def rewrite_asserts(
|
||||||
|
|
||||||
|
|
||||||
def _saferepr(obj: object) -> str:
|
def _saferepr(obj: object) -> str:
|
||||||
"""Get a safe repr of an object for assertion error messages.
|
r"""Get a safe repr of an object for assertion error messages.
|
||||||
|
|
||||||
The assertion formatting (util.format_explanation()) requires
|
The assertion formatting (util.format_explanation()) requires
|
||||||
newlines to be escaped since they are a special character for it.
|
newlines to be escaped since they are a special character for it.
|
||||||
|
@ -419,18 +419,16 @@ def _saferepr(obj: object) -> str:
|
||||||
custom repr it is possible to contain one of the special escape
|
custom repr it is possible to contain one of the special escape
|
||||||
sequences, especially '\n{' and '\n}' are likely to be present in
|
sequences, especially '\n{' and '\n}' are likely to be present in
|
||||||
JSON reprs.
|
JSON reprs.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return saferepr(obj).replace("\n", "\\n")
|
return saferepr(obj).replace("\n", "\\n")
|
||||||
|
|
||||||
|
|
||||||
def _format_assertmsg(obj: object) -> str:
|
def _format_assertmsg(obj: object) -> str:
|
||||||
"""Format the custom assertion message given.
|
r"""Format the custom assertion message given.
|
||||||
|
|
||||||
For strings this simply replaces newlines with '\n~' so that
|
For strings this simply replaces newlines with '\n~' so that
|
||||||
util.format_explanation() will preserve them instead of escaping
|
util.format_explanation() will preserve them instead of escaping
|
||||||
newlines. For other objects saferepr() is used first.
|
newlines. For other objects saferepr() is used first.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# reprlib appears to have a bug which means that if a string
|
# reprlib appears to have a bug which means that if a string
|
||||||
# contains a newline it gets escaped, however if an object has a
|
# contains a newline it gets escaped, however if an object has a
|
||||||
|
@ -491,8 +489,8 @@ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _check_if_assertion_pass_impl() -> bool:
|
def _check_if_assertion_pass_impl() -> bool:
|
||||||
"""Checks if any plugins implement the pytest_assertion_pass hook
|
"""Check if any plugins implement the pytest_assertion_pass hook
|
||||||
in order not to generate explanation unecessarily (might be expensive)"""
|
in order not to generate explanation unecessarily (might be expensive)."""
|
||||||
return True if util._assertion_pass else False
|
return True if util._assertion_pass else False
|
||||||
|
|
||||||
|
|
||||||
|
@ -541,7 +539,7 @@ def set_location(node, lineno, col_offset):
|
||||||
|
|
||||||
|
|
||||||
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
|
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
|
||||||
"""Returns a mapping from {lineno: "assertion test expression"}"""
|
"""Return a mapping from {lineno: "assertion test expression"}."""
|
||||||
ret = {} # type: Dict[int, str]
|
ret = {} # type: Dict[int, str]
|
||||||
|
|
||||||
depth = 0
|
depth = 0
|
||||||
|
@ -645,7 +643,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
|
|
||||||
This state is reset on every new assert statement visited and used
|
This state is reset on every new assert statement visited and used
|
||||||
by the other visitors.
|
by the other visitors.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -770,7 +767,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
current formatting context, e.g. ``%(py0)s``. The placeholder
|
current formatting context, e.g. ``%(py0)s``. The placeholder
|
||||||
and expr are placed in the current format context so that it
|
and expr are placed in the current format context so that it
|
||||||
can be used on the next call to .pop_format_context().
|
can be used on the next call to .pop_format_context().
|
||||||
|
|
||||||
"""
|
"""
|
||||||
specifier = "py" + str(next(self.variable_counter))
|
specifier = "py" + str(next(self.variable_counter))
|
||||||
self.explanation_specifiers[specifier] = expr
|
self.explanation_specifiers[specifier] = expr
|
||||||
|
@ -785,7 +781,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
.explanation_param(). Finally .pop_format_context() is used
|
.explanation_param(). Finally .pop_format_context() is used
|
||||||
to format a string of %-formatted values as added by
|
to format a string of %-formatted values as added by
|
||||||
.explanation_param().
|
.explanation_param().
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.explanation_specifiers = {} # type: Dict[str, ast.expr]
|
self.explanation_specifiers = {} # type: Dict[str, ast.expr]
|
||||||
self.stack.append(self.explanation_specifiers)
|
self.stack.append(self.explanation_specifiers)
|
||||||
|
@ -797,7 +792,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
the %-placeholders created by .explanation_param(). This will
|
the %-placeholders created by .explanation_param(). This will
|
||||||
add the required code to format said string to .expl_stmts and
|
add the required code to format said string to .expl_stmts and
|
||||||
return the ast.Name instance of the formatted string.
|
return the ast.Name instance of the formatted string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
current = self.stack.pop()
|
current = self.stack.pop()
|
||||||
if self.stack:
|
if self.stack:
|
||||||
|
@ -824,7 +818,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
intermediate values and replace it with an if statement which
|
intermediate values and replace it with an if statement which
|
||||||
raises an assertion error with a detailed explanation in case
|
raises an assertion error with a detailed explanation in case
|
||||||
the expression is false.
|
the expression is false.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
||||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||||
|
@ -994,9 +987,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
return res, explanation
|
return res, explanation
|
||||||
|
|
||||||
def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
|
def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
|
||||||
"""
|
|
||||||
visit `ast.Call` nodes
|
|
||||||
"""
|
|
||||||
new_func, func_expl = self.visit(call.func)
|
new_func, func_expl = self.visit(call.func)
|
||||||
arg_expls = []
|
arg_expls = []
|
||||||
new_args = []
|
new_args = []
|
||||||
|
@ -1021,7 +1011,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
return res, outer_expl
|
return res, outer_expl
|
||||||
|
|
||||||
def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
|
def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
|
||||||
# From Python 3.5, a Starred node can appear in a function call
|
# From Python 3.5, a Starred node can appear in a function call.
|
||||||
res, expl = self.visit(starred.value)
|
res, expl = self.visit(starred.value)
|
||||||
new_starred = ast.Starred(res, starred.ctx)
|
new_starred = ast.Starred(res, starred.ctx)
|
||||||
return new_starred, "*" + expl
|
return new_starred, "*" + expl
|
||||||
|
@ -1076,8 +1066,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
|
|
||||||
|
|
||||||
def try_makedirs(cache_dir: Path) -> bool:
|
def try_makedirs(cache_dir: Path) -> bool:
|
||||||
"""Attempts to create the given directory and sub-directories exist, returns True if
|
"""Attempt to create the given directory and sub-directories exist.
|
||||||
successful or it already exists"""
|
|
||||||
|
Returns True if successful or if it already exists.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
os.makedirs(fspath(cache_dir), exist_ok=True)
|
os.makedirs(fspath(cache_dir), exist_ok=True)
|
||||||
except (FileNotFoundError, NotADirectoryError, FileExistsError):
|
except (FileNotFoundError, NotADirectoryError, FileExistsError):
|
||||||
|
@ -1096,7 +1088,7 @@ def try_makedirs(cache_dir: Path) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def get_cache_dir(file_path: Path) -> Path:
|
def get_cache_dir(file_path: Path) -> Path:
|
||||||
"""Returns the cache directory to write .pyc files for the given .py file path"""
|
"""Return the cache directory to write .pyc files for the given .py file path."""
|
||||||
if sys.version_info >= (3, 8) and sys.pycache_prefix:
|
if sys.version_info >= (3, 8) and sys.pycache_prefix:
|
||||||
# given:
|
# given:
|
||||||
# prefix = '/tmp/pycs'
|
# prefix = '/tmp/pycs'
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Utilities for truncating assertion output.
|
||||||
Utilities for truncating assertion output.
|
|
||||||
|
|
||||||
Current default behaviour is to truncate assertion explanations at
|
Current default behaviour is to truncate assertion explanations at
|
||||||
~8 terminal lines, unless running in "-vv" mode or running on CI.
|
~8 terminal lines, unless running in "-vv" mode or running on CI.
|
||||||
|
@ -19,18 +18,14 @@ USAGE_MSG = "use '-vv' to show"
|
||||||
def truncate_if_required(
|
def truncate_if_required(
|
||||||
explanation: List[str], item: Item, max_length: Optional[int] = None
|
explanation: List[str], item: Item, max_length: Optional[int] = None
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""Truncate this assertion explanation if the given test item is eligible."""
|
||||||
Truncate this assertion explanation if the given test item is eligible.
|
|
||||||
"""
|
|
||||||
if _should_truncate_item(item):
|
if _should_truncate_item(item):
|
||||||
return _truncate_explanation(explanation)
|
return _truncate_explanation(explanation)
|
||||||
return explanation
|
return explanation
|
||||||
|
|
||||||
|
|
||||||
def _should_truncate_item(item: Item) -> bool:
|
def _should_truncate_item(item: Item) -> bool:
|
||||||
"""
|
"""Whether or not this test item is eligible for truncation."""
|
||||||
Whether or not this test item is eligible for truncation.
|
|
||||||
"""
|
|
||||||
verbose = item.config.option.verbose
|
verbose = item.config.option.verbose
|
||||||
return verbose < 2 and not _running_on_ci()
|
return verbose < 2 and not _running_on_ci()
|
||||||
|
|
||||||
|
@ -46,8 +41,7 @@ def _truncate_explanation(
|
||||||
max_lines: Optional[int] = None,
|
max_lines: Optional[int] = None,
|
||||||
max_chars: Optional[int] = None,
|
max_chars: Optional[int] = None,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""Truncate given list of strings that makes up the assertion explanation.
|
||||||
Truncate given list of strings that makes up the assertion explanation.
|
|
||||||
|
|
||||||
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
||||||
first. The remaining lines will be replaced by a usage message.
|
first. The remaining lines will be replaced by a usage message.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Utilities for assertion debugging"""
|
"""Utilities for assertion debugging."""
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import pprint
|
import pprint
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
|
@ -30,7 +30,7 @@ _assertion_pass = None # type: Optional[Callable[[int, str, str], None]]
|
||||||
|
|
||||||
|
|
||||||
def format_explanation(explanation: str) -> str:
|
def format_explanation(explanation: str) -> str:
|
||||||
"""This formats an explanation
|
r"""Format an explanation.
|
||||||
|
|
||||||
Normally all embedded newlines are escaped, however there are
|
Normally all embedded newlines are escaped, however there are
|
||||||
three exceptions: \n{, \n} and \n~. The first two are intended
|
three exceptions: \n{, \n} and \n~. The first two are intended
|
||||||
|
@ -45,7 +45,7 @@ def format_explanation(explanation: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _split_explanation(explanation: str) -> List[str]:
|
def _split_explanation(explanation: str) -> List[str]:
|
||||||
"""Return a list of individual lines in the explanation
|
r"""Return a list of individual lines in the explanation.
|
||||||
|
|
||||||
This will return a list of lines split on '\n{', '\n}' and '\n~'.
|
This will return a list of lines split on '\n{', '\n}' and '\n~'.
|
||||||
Any other newlines will be escaped and appear in the line as the
|
Any other newlines will be escaped and appear in the line as the
|
||||||
|
@ -62,11 +62,11 @@ def _split_explanation(explanation: str) -> List[str]:
|
||||||
|
|
||||||
|
|
||||||
def _format_lines(lines: Sequence[str]) -> List[str]:
|
def _format_lines(lines: Sequence[str]) -> List[str]:
|
||||||
"""Format the individual lines
|
"""Format the individual lines.
|
||||||
|
|
||||||
This will replace the '{', '}' and '~' characters of our mini
|
This will replace the '{', '}' and '~' characters of our mini formatting
|
||||||
formatting language with the proper 'where ...', 'and ...' and ' +
|
language with the proper 'where ...', 'and ...' and ' + ...' text, taking
|
||||||
...' text, taking care of indentation along the way.
|
care of indentation along the way.
|
||||||
|
|
||||||
Return a list of formatted lines.
|
Return a list of formatted lines.
|
||||||
"""
|
"""
|
||||||
|
@ -129,7 +129,7 @@ def isiterable(obj: Any) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
|
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
|
||||||
"""Return specialised explanations for some operators/operands"""
|
"""Return specialised explanations for some operators/operands."""
|
||||||
verbose = config.getoption("verbose")
|
verbose = config.getoption("verbose")
|
||||||
if verbose > 1:
|
if verbose > 1:
|
||||||
left_repr = safeformat(left)
|
left_repr = safeformat(left)
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
"""
|
"""Implementation of the cache provider."""
|
||||||
merged implementation of the cache provider
|
# This plugin was not named "cache" to avoid conflicts with the external
|
||||||
|
# pytest-cache version.
|
||||||
the name cache was not chosen to ensure pluggy automatically
|
|
||||||
ignores the external pytest-cache
|
|
||||||
"""
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
@ -73,7 +70,7 @@ class Cache:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear_cache(cls, cachedir: Path) -> None:
|
def clear_cache(cls, cachedir: Path) -> None:
|
||||||
"""Clears the sub-directories used to hold cached directories and values."""
|
"""Clear the sub-directories used to hold cached directories and values."""
|
||||||
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
|
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
|
||||||
d = cachedir / prefix
|
d = cachedir / prefix
|
||||||
if d.is_dir():
|
if d.is_dir():
|
||||||
|
@ -94,14 +91,16 @@ class Cache:
|
||||||
)
|
)
|
||||||
|
|
||||||
def makedir(self, name: str) -> py.path.local:
|
def makedir(self, name: str) -> py.path.local:
|
||||||
""" return a directory path object with the given name. If the
|
"""Return a directory path object with the given name.
|
||||||
directory does not yet exist, it will be created. You can use it
|
|
||||||
to manage files likes e. g. store/retrieve database
|
|
||||||
dumps across test sessions.
|
|
||||||
|
|
||||||
:param name: must be a string not containing a ``/`` separator.
|
If the directory does not yet exist, it will be created. You can use
|
||||||
Make sure the name contains your plugin or application
|
it to manage files to e.g. store/retrieve database dumps across test
|
||||||
identifiers to prevent clashes with other cache users.
|
sessions.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
Must be a string not containing a ``/`` separator.
|
||||||
|
Make sure the name contains your plugin or application
|
||||||
|
identifiers to prevent clashes with other cache users.
|
||||||
"""
|
"""
|
||||||
path = Path(name)
|
path = Path(name)
|
||||||
if len(path.parts) > 1:
|
if len(path.parts) > 1:
|
||||||
|
@ -114,15 +113,16 @@ class Cache:
|
||||||
return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
|
return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
|
||||||
|
|
||||||
def get(self, key: str, default):
|
def get(self, key: str, default):
|
||||||
""" return cached value for the given key. If no value
|
"""Return the cached value for the given key.
|
||||||
was yet cached or the value cannot be read, the specified
|
|
||||||
|
If no value was yet cached or the value cannot be read, the specified
|
||||||
default is returned.
|
default is returned.
|
||||||
|
|
||||||
:param key: must be a ``/`` separated value. Usually the first
|
:param key:
|
||||||
name is the name of your plugin or your application.
|
Must be a ``/`` separated value. Usually the first
|
||||||
:param default: must be provided in case of a cache-miss or
|
name is the name of your plugin or your application.
|
||||||
invalid cache values.
|
:param default:
|
||||||
|
The value to return in case of a cache-miss or invalid cache value.
|
||||||
"""
|
"""
|
||||||
path = self._getvaluepath(key)
|
path = self._getvaluepath(key)
|
||||||
try:
|
try:
|
||||||
|
@ -132,13 +132,14 @@ class Cache:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def set(self, key: str, value: object) -> None:
|
def set(self, key: str, value: object) -> None:
|
||||||
""" save value for the given key.
|
"""Save value for the given key.
|
||||||
|
|
||||||
:param key: must be a ``/`` separated value. Usually the first
|
:param key:
|
||||||
name is the name of your plugin or your application.
|
Must be a ``/`` separated value. Usually the first
|
||||||
:param value: must be of any combination of basic
|
name is the name of your plugin or your application.
|
||||||
python types, including nested types
|
:param value:
|
||||||
like e. g. lists of dictionaries.
|
Must be of any combination of basic python types,
|
||||||
|
including nested types like lists of dictionaries.
|
||||||
"""
|
"""
|
||||||
path = self._getvaluepath(key)
|
path = self._getvaluepath(key)
|
||||||
try:
|
try:
|
||||||
|
@ -241,7 +242,7 @@ class LFPluginCollSkipfiles:
|
||||||
|
|
||||||
|
|
||||||
class LFPlugin:
|
class LFPlugin:
|
||||||
""" Plugin which implements the --lf (run last-failing) option """
|
"""Plugin which implements the --lf (run last-failing) option."""
|
||||||
|
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -262,7 +263,7 @@ class LFPlugin:
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_last_failed_paths(self) -> Set[Path]:
|
def get_last_failed_paths(self) -> Set[Path]:
|
||||||
"""Returns a set with all Paths()s of the previously failed nodeids."""
|
"""Return a set with all Paths()s of the previously failed nodeids."""
|
||||||
rootpath = Path(str(self.config.rootdir))
|
rootpath = Path(str(self.config.rootdir))
|
||||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
||||||
return {x for x in result if x.exists()}
|
return {x for x in result if x.exists()}
|
||||||
|
@ -351,7 +352,7 @@ class LFPlugin:
|
||||||
|
|
||||||
|
|
||||||
class NFPlugin:
|
class NFPlugin:
|
||||||
""" Plugin which implements the --nf (run new-first) option """
|
"""Plugin which implements the --nf (run new-first) option."""
|
||||||
|
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -471,13 +472,12 @@ def pytest_configure(config: Config) -> None:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cache(request: FixtureRequest) -> Cache:
|
def cache(request: FixtureRequest) -> Cache:
|
||||||
"""
|
"""Return a cache object that can persist state between testing sessions.
|
||||||
Return a cache object that can persist state between testing sessions.
|
|
||||||
|
|
||||||
cache.get(key, default)
|
cache.get(key, default)
|
||||||
cache.set(key, value)
|
cache.set(key, value)
|
||||||
|
|
||||||
Keys must be a ``/`` separated value, where the first part is usually the
|
Keys must be ``/`` separated strings, where the first part is usually the
|
||||||
name of your plugin or application to avoid clashes with other cache users.
|
name of your plugin or application to avoid clashes with other cache users.
|
||||||
|
|
||||||
Values can be any object handled by the json stdlib module.
|
Values can be any object handled by the json stdlib module.
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
"""
|
"""Per-test stdout/stderr capturing mechanism."""
|
||||||
per-test stdout/stderr capturing mechanism.
|
|
||||||
|
|
||||||
"""
|
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
|
@ -49,8 +46,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _colorama_workaround() -> None:
|
def _colorama_workaround() -> None:
|
||||||
"""
|
"""Ensure colorama is imported so that it attaches to the correct stdio
|
||||||
Ensure colorama is imported so that it attaches to the correct stdio
|
|
||||||
handles on Windows.
|
handles on Windows.
|
||||||
|
|
||||||
colorama uses the terminal on import time. So if something does the
|
colorama uses the terminal on import time. So if something does the
|
||||||
|
@ -65,8 +61,7 @@ def _colorama_workaround() -> None:
|
||||||
|
|
||||||
|
|
||||||
def _readline_workaround() -> None:
|
def _readline_workaround() -> None:
|
||||||
"""
|
"""Ensure readline is imported so that it attaches to the correct stdio
|
||||||
Ensure readline is imported so that it attaches to the correct stdio
|
|
||||||
handles on Windows.
|
handles on Windows.
|
||||||
|
|
||||||
Pdb uses readline support where available--when not running from the Python
|
Pdb uses readline support where available--when not running from the Python
|
||||||
|
@ -80,7 +75,7 @@ def _readline_workaround() -> None:
|
||||||
workaround ensures that readline is imported before I/O capture is setup so
|
workaround ensures that readline is imported before I/O capture is setup so
|
||||||
that it can attach to the actual stdin/out for the console.
|
that it can attach to the actual stdin/out for the console.
|
||||||
|
|
||||||
See https://github.com/pytest-dev/pytest/pull/1281
|
See https://github.com/pytest-dev/pytest/pull/1281.
|
||||||
"""
|
"""
|
||||||
if sys.platform.startswith("win32"):
|
if sys.platform.startswith("win32"):
|
||||||
try:
|
try:
|
||||||
|
@ -90,8 +85,9 @@ def _readline_workaround() -> None:
|
||||||
|
|
||||||
|
|
||||||
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||||
"""
|
"""Workaround for Windows Unicode console handling on Python>=3.6.
|
||||||
Python 3.6 implemented unicode console handling for Windows. This works
|
|
||||||
|
Python 3.6 implemented Unicode console handling for Windows. This works
|
||||||
by reading/writing to the raw console handle using
|
by reading/writing to the raw console handle using
|
||||||
``{Read,Write}ConsoleW``.
|
``{Read,Write}ConsoleW``.
|
||||||
|
|
||||||
|
@ -106,10 +102,11 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||||
also means a different handle by replicating the logic in
|
also means a different handle by replicating the logic in
|
||||||
"Py_lifecycle.c:initstdio/create_stdio".
|
"Py_lifecycle.c:initstdio/create_stdio".
|
||||||
|
|
||||||
:param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given
|
:param stream:
|
||||||
|
In practice ``sys.stdout`` or ``sys.stderr``, but given
|
||||||
here as parameter for unittesting purposes.
|
here as parameter for unittesting purposes.
|
||||||
|
|
||||||
See https://github.com/pytest-dev/py/issues/103
|
See https://github.com/pytest-dev/py/issues/103.
|
||||||
"""
|
"""
|
||||||
if (
|
if (
|
||||||
not sys.platform.startswith("win32")
|
not sys.platform.startswith("win32")
|
||||||
|
@ -118,7 +115,7 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666)
|
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
|
||||||
if not hasattr(stream, "buffer"):
|
if not hasattr(stream, "buffer"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -158,10 +155,10 @@ def pytest_load_initial_conftests(early_config: Config):
|
||||||
capman = CaptureManager(ns.capture)
|
capman = CaptureManager(ns.capture)
|
||||||
pluginmanager.register(capman, "capturemanager")
|
pluginmanager.register(capman, "capturemanager")
|
||||||
|
|
||||||
# make sure that capturemanager is properly reset at final shutdown
|
# Make sure that capturemanager is properly reset at final shutdown.
|
||||||
early_config.add_cleanup(capman.stop_global_capturing)
|
early_config.add_cleanup(capman.stop_global_capturing)
|
||||||
|
|
||||||
# finally trigger conftest loading but while capturing (issue93)
|
# Finally trigger conftest loading but while capturing (issue #93).
|
||||||
capman.start_global_capturing()
|
capman.start_global_capturing()
|
||||||
outcome = yield
|
outcome = yield
|
||||||
capman.suspend_global_capture()
|
capman.suspend_global_capture()
|
||||||
|
@ -347,9 +344,9 @@ class SysCapture(SysCaptureBinary):
|
||||||
|
|
||||||
|
|
||||||
class FDCaptureBinary:
|
class FDCaptureBinary:
|
||||||
"""Capture IO to/from a given os-level filedescriptor.
|
"""Capture IO to/from a given OS-level file descriptor.
|
||||||
|
|
||||||
snap() produces `bytes`
|
snap() produces `bytes`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EMPTY_BUFFER = b""
|
EMPTY_BUFFER = b""
|
||||||
|
@ -415,7 +412,7 @@ class FDCaptureBinary:
|
||||||
)
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
""" Start capturing on targetfd using memorized tmpfile. """
|
"""Start capturing on targetfd using memorized tmpfile."""
|
||||||
self._assert_state("start", ("initialized",))
|
self._assert_state("start", ("initialized",))
|
||||||
os.dup2(self.tmpfile.fileno(), self.targetfd)
|
os.dup2(self.tmpfile.fileno(), self.targetfd)
|
||||||
self.syscapture.start()
|
self.syscapture.start()
|
||||||
|
@ -430,8 +427,8 @@ class FDCaptureBinary:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def done(self) -> None:
|
def done(self) -> None:
|
||||||
""" stop capturing, restore streams, return original capture file,
|
"""Stop capturing, restore streams, return original capture file,
|
||||||
seeked to position zero. """
|
seeked to position zero."""
|
||||||
self._assert_state("done", ("initialized", "started", "suspended", "done"))
|
self._assert_state("done", ("initialized", "started", "suspended", "done"))
|
||||||
if self._state == "done":
|
if self._state == "done":
|
||||||
return
|
return
|
||||||
|
@ -462,15 +459,15 @@ class FDCaptureBinary:
|
||||||
self._state = "started"
|
self._state = "started"
|
||||||
|
|
||||||
def writeorg(self, data):
|
def writeorg(self, data):
|
||||||
""" write to original file descriptor. """
|
"""Write to original file descriptor."""
|
||||||
self._assert_state("writeorg", ("started", "suspended"))
|
self._assert_state("writeorg", ("started", "suspended"))
|
||||||
os.write(self.targetfd_save, data)
|
os.write(self.targetfd_save, data)
|
||||||
|
|
||||||
|
|
||||||
class FDCapture(FDCaptureBinary):
|
class FDCapture(FDCaptureBinary):
|
||||||
"""Capture IO to/from a given os-level filedescriptor.
|
"""Capture IO to/from a given OS-level file descriptor.
|
||||||
|
|
||||||
snap() produces text
|
snap() produces text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Ignore type because it doesn't match the type in the superclass (bytes).
|
# Ignore type because it doesn't match the type in the superclass (bytes).
|
||||||
|
@ -485,7 +482,7 @@ class FDCapture(FDCaptureBinary):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def writeorg(self, data):
|
def writeorg(self, data):
|
||||||
""" write to original file descriptor. """
|
"""Write to original file descriptor."""
|
||||||
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
|
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
|
||||||
|
|
||||||
|
|
||||||
|
@ -518,7 +515,7 @@ class MultiCapture:
|
||||||
self.err.start()
|
self.err.start()
|
||||||
|
|
||||||
def pop_outerr_to_orig(self):
|
def pop_outerr_to_orig(self):
|
||||||
""" pop current snapshot out/err capture and flush to orig streams. """
|
"""Pop current snapshot out/err capture and flush to orig streams."""
|
||||||
out, err = self.readouterr()
|
out, err = self.readouterr()
|
||||||
if out:
|
if out:
|
||||||
self.out.writeorg(out)
|
self.out.writeorg(out)
|
||||||
|
@ -547,7 +544,7 @@ class MultiCapture:
|
||||||
self._in_suspended = False
|
self._in_suspended = False
|
||||||
|
|
||||||
def stop_capturing(self) -> None:
|
def stop_capturing(self) -> None:
|
||||||
""" stop capturing and reset capturing streams """
|
"""Stop capturing and reset capturing streams."""
|
||||||
if self._state == "stopped":
|
if self._state == "stopped":
|
||||||
raise ValueError("was already stopped")
|
raise ValueError("was already stopped")
|
||||||
self._state = "stopped"
|
self._state = "stopped"
|
||||||
|
@ -588,16 +585,22 @@ def _get_multicapture(method: "_CaptureMethod") -> MultiCapture:
|
||||||
|
|
||||||
|
|
||||||
class CaptureManager:
|
class CaptureManager:
|
||||||
"""
|
"""The capture plugin.
|
||||||
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
|
|
||||||
test phase (setup, call, teardown). After each of those points, the captured output is obtained and
|
Manages that the appropriate capture method is enabled/disabled during
|
||||||
attached to the collection/runtest report.
|
collection and each test phase (setup, call, teardown). After each of
|
||||||
|
those points, the captured output is obtained and attached to the
|
||||||
|
collection/runtest report.
|
||||||
|
|
||||||
There are two levels of capture:
|
There are two levels of capture:
|
||||||
* global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled
|
|
||||||
during collection and each test phase.
|
* global: enabled by default and can be suppressed by the ``-s``
|
||||||
* fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this
|
option. This is always enabled/disabled during collection and each test
|
||||||
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
phase.
|
||||||
|
|
||||||
|
* fixture: when a test function or one of its fixture depend on the
|
||||||
|
``capsys`` or ``capfd`` fixtures. In this case special handling is
|
||||||
|
needed to ensure the fixtures take precedence over the global capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, method: "_CaptureMethod") -> None:
|
def __init__(self, method: "_CaptureMethod") -> None:
|
||||||
|
@ -673,14 +676,13 @@ class CaptureManager:
|
||||||
self._capture_fixture = None
|
self._capture_fixture = None
|
||||||
|
|
||||||
def activate_fixture(self) -> None:
|
def activate_fixture(self) -> None:
|
||||||
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
|
"""If the current item is using ``capsys`` or ``capfd``, activate
|
||||||
the global capture.
|
them so they take precedence over the global capture."""
|
||||||
"""
|
|
||||||
if self._capture_fixture:
|
if self._capture_fixture:
|
||||||
self._capture_fixture._start()
|
self._capture_fixture._start()
|
||||||
|
|
||||||
def deactivate_fixture(self) -> None:
|
def deactivate_fixture(self) -> None:
|
||||||
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
"""Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
||||||
if self._capture_fixture:
|
if self._capture_fixture:
|
||||||
self._capture_fixture.close()
|
self._capture_fixture.close()
|
||||||
|
|
||||||
|
@ -759,10 +761,8 @@ class CaptureManager:
|
||||||
|
|
||||||
|
|
||||||
class CaptureFixture:
|
class CaptureFixture:
|
||||||
"""
|
"""Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`,
|
||||||
Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary`
|
:py:func:`capfd` and :py:func:`capfdbinary` fixtures."""
|
||||||
fixtures.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, captureclass, request: SubRequest) -> None:
|
def __init__(self, captureclass, request: SubRequest) -> None:
|
||||||
self.captureclass = captureclass
|
self.captureclass = captureclass
|
||||||
|
@ -787,9 +787,12 @@ class CaptureFixture:
|
||||||
self._capture = None
|
self._capture = None
|
||||||
|
|
||||||
def readouterr(self):
|
def readouterr(self):
|
||||||
"""Read and return the captured output so far, resetting the internal buffer.
|
"""Read and return the captured output so far, resetting the internal
|
||||||
|
buffer.
|
||||||
|
|
||||||
:return: captured content as a namedtuple with ``out`` and ``err`` string attributes
|
:returns:
|
||||||
|
The captured content as a namedtuple with ``out`` and ``err``
|
||||||
|
string attributes.
|
||||||
"""
|
"""
|
||||||
captured_out, captured_err = self._captured_out, self._captured_err
|
captured_out, captured_err = self._captured_out, self._captured_err
|
||||||
if self._capture is not None:
|
if self._capture is not None:
|
||||||
|
@ -801,18 +804,18 @@ class CaptureFixture:
|
||||||
return CaptureResult(captured_out, captured_err)
|
return CaptureResult(captured_out, captured_err)
|
||||||
|
|
||||||
def _suspend(self) -> None:
|
def _suspend(self) -> None:
|
||||||
"""Suspends this fixture's own capturing temporarily."""
|
"""Suspend this fixture's own capturing temporarily."""
|
||||||
if self._capture is not None:
|
if self._capture is not None:
|
||||||
self._capture.suspend_capturing()
|
self._capture.suspend_capturing()
|
||||||
|
|
||||||
def _resume(self) -> None:
|
def _resume(self) -> None:
|
||||||
"""Resumes this fixture's own capturing temporarily."""
|
"""Resume this fixture's own capturing temporarily."""
|
||||||
if self._capture is not None:
|
if self._capture is not None:
|
||||||
self._capture.resume_capturing()
|
self._capture.resume_capturing()
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def disabled(self) -> Generator[None, None, None]:
|
def disabled(self) -> Generator[None, None, None]:
|
||||||
"""Temporarily disables capture while inside the 'with' block."""
|
"""Temporarily disable capturing while inside the ``with`` block."""
|
||||||
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
|
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
|
||||||
with capmanager.global_and_fixture_disabled():
|
with capmanager.global_and_fixture_disabled():
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
"""
|
"""Python version compatibility code."""
|
||||||
python version compatibility code
|
|
||||||
"""
|
|
||||||
import enum
|
import enum
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -73,8 +71,7 @@ if sys.version_info < (3, 6):
|
||||||
|
|
||||||
def fspath(p):
|
def fspath(p):
|
||||||
"""os.fspath replacement, useful to point out when we should replace it by the
|
"""os.fspath replacement, useful to point out when we should replace it by the
|
||||||
real function once we drop py35.
|
real function once we drop py35."""
|
||||||
"""
|
|
||||||
return str(p)
|
return str(p)
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,8 +85,7 @@ def is_generator(func: object) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def iscoroutinefunction(func: object) -> bool:
|
def iscoroutinefunction(func: object) -> bool:
|
||||||
"""
|
"""Return True if func is a coroutine function (a function defined with async
|
||||||
Return True if func is a coroutine function (a function defined with async
|
|
||||||
def syntax, and doesn't contain yield), or a function decorated with
|
def syntax, and doesn't contain yield), or a function decorated with
|
||||||
@asyncio.coroutine.
|
@asyncio.coroutine.
|
||||||
|
|
||||||
|
@ -101,7 +97,8 @@ def iscoroutinefunction(func: object) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def is_async_function(func: object) -> bool:
|
def is_async_function(func: object) -> bool:
|
||||||
"""Return True if the given function seems to be an async function or async generator"""
|
"""Return True if the given function seems to be an async function or
|
||||||
|
an async generator."""
|
||||||
return iscoroutinefunction(func) or (
|
return iscoroutinefunction(func) or (
|
||||||
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
|
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
|
||||||
)
|
)
|
||||||
|
@ -119,7 +116,7 @@ def getlocation(function, curdir=None) -> str:
|
||||||
|
|
||||||
|
|
||||||
def num_mock_patch_args(function) -> int:
|
def num_mock_patch_args(function) -> int:
|
||||||
""" return number of arguments used up by mock arguments (if any) """
|
"""Return number of arguments used up by mock arguments (if any)."""
|
||||||
patchings = getattr(function, "patchings", None)
|
patchings = getattr(function, "patchings", None)
|
||||||
if not patchings:
|
if not patchings:
|
||||||
return 0
|
return 0
|
||||||
|
@ -144,13 +141,13 @@ def getfuncargnames(
|
||||||
is_method: bool = False,
|
is_method: bool = False,
|
||||||
cls: Optional[type] = None
|
cls: Optional[type] = None
|
||||||
) -> Tuple[str, ...]:
|
) -> Tuple[str, ...]:
|
||||||
"""Returns the names of a function's mandatory arguments.
|
"""Return the names of a function's mandatory arguments.
|
||||||
|
|
||||||
This should return the names of all function arguments that:
|
Should return the names of all function arguments that:
|
||||||
* Aren't bound to an instance or type as in instance or class methods.
|
* Aren't bound to an instance or type as in instance or class methods.
|
||||||
* Don't have default values.
|
* Don't have default values.
|
||||||
* Aren't bound with functools.partial.
|
* Aren't bound with functools.partial.
|
||||||
* Aren't replaced with mocks.
|
* Aren't replaced with mocks.
|
||||||
|
|
||||||
The is_method and cls arguments indicate that the function should
|
The is_method and cls arguments indicate that the function should
|
||||||
be treated as a bound method even though it's not unless, only in
|
be treated as a bound method even though it's not unless, only in
|
||||||
|
@ -212,8 +209,9 @@ else:
|
||||||
|
|
||||||
|
|
||||||
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
||||||
# Note: this code intentionally mirrors the code at the beginning of getfuncargnames,
|
# Note: this code intentionally mirrors the code at the beginning of
|
||||||
# to get the arguments which were excluded from its result because they had default values
|
# getfuncargnames, to get the arguments which were excluded from its result
|
||||||
|
# because they had default values.
|
||||||
return tuple(
|
return tuple(
|
||||||
p.name
|
p.name
|
||||||
for p in signature(function).parameters.values()
|
for p in signature(function).parameters.values()
|
||||||
|
@ -242,22 +240,21 @@ def _bytes_to_ascii(val: bytes) -> str:
|
||||||
|
|
||||||
|
|
||||||
def ascii_escaped(val: Union[bytes, str]) -> str:
|
def ascii_escaped(val: Union[bytes, str]) -> str:
|
||||||
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
|
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||||
bytes objects into a sequence of escaped bytes:
|
bytes objects into a sequence of escaped bytes:
|
||||||
|
|
||||||
b'\xc3\xb4\xc5\xd6' -> '\\xc3\\xb4\\xc5\\xd6'
|
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
|
||||||
|
|
||||||
and escapes unicode objects into a sequence of escaped unicode
|
and escapes unicode objects into a sequence of escaped unicode
|
||||||
ids, e.g.:
|
ids, e.g.:
|
||||||
|
|
||||||
'4\\nV\\U00043efa\\x0eMXWB\\x1e\\u3028\\u15fd\\xcd\\U0007d944'
|
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
|
||||||
|
|
||||||
note:
|
Note:
|
||||||
the obvious "v.decode('unicode-escape')" will return
|
The obvious "v.decode('unicode-escape')" will return
|
||||||
valid utf-8 unicode if it finds them in bytes, but we
|
valid UTF-8 unicode if it finds them in bytes, but we
|
||||||
want to return escaped bytes for any byte, even if they match
|
want to return escaped bytes for any byte, even if they match
|
||||||
a utf-8 string.
|
a UTF-8 string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(val, bytes):
|
if isinstance(val, bytes):
|
||||||
ret = _bytes_to_ascii(val)
|
ret = _bytes_to_ascii(val)
|
||||||
|
@ -270,18 +267,17 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
|
||||||
class _PytestWrapper:
|
class _PytestWrapper:
|
||||||
"""Dummy wrapper around a function object for internal use only.
|
"""Dummy wrapper around a function object for internal use only.
|
||||||
|
|
||||||
Used to correctly unwrap the underlying function object
|
Used to correctly unwrap the underlying function object when we are
|
||||||
when we are creating fixtures, because we wrap the function object ourselves with a decorator
|
creating fixtures, because we wrap the function object ourselves with a
|
||||||
to issue warnings when the fixture function is called directly.
|
decorator to issue warnings when the fixture function is called directly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
obj = attr.ib()
|
obj = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
def get_real_func(obj):
|
def get_real_func(obj):
|
||||||
""" gets the real function object of the (possibly) wrapped object by
|
"""Get the real function object of the (possibly) wrapped object by
|
||||||
functools.wraps or functools.partial.
|
functools.wraps or functools.partial."""
|
||||||
"""
|
|
||||||
start_obj = obj
|
start_obj = obj
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
|
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
|
||||||
|
@ -307,10 +303,9 @@ def get_real_func(obj):
|
||||||
|
|
||||||
|
|
||||||
def get_real_method(obj, holder):
|
def get_real_method(obj, holder):
|
||||||
"""
|
"""Attempt to obtain the real function object that might be wrapping
|
||||||
Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time
|
``obj``, while at the same time returning a bound method to ``holder`` if
|
||||||
returning a bound method to ``holder`` if the original object was a bound method.
|
the original object was a bound method."""
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
is_method = hasattr(obj, "__func__")
|
is_method = hasattr(obj, "__func__")
|
||||||
obj = get_real_func(obj)
|
obj = get_real_func(obj)
|
||||||
|
@ -329,12 +324,13 @@ def getimfunc(func):
|
||||||
|
|
||||||
|
|
||||||
def safe_getattr(object: Any, name: str, default: Any) -> Any:
|
def safe_getattr(object: Any, name: str, default: Any) -> Any:
|
||||||
""" Like getattr but return default upon any Exception or any OutcomeException.
|
"""Like getattr but return default upon any Exception or any OutcomeException.
|
||||||
|
|
||||||
Attribute access can potentially fail for 'evil' Python objects.
|
Attribute access can potentially fail for 'evil' Python objects.
|
||||||
See issue #214.
|
See issue #214.
|
||||||
It catches OutcomeException because of #2490 (issue #580), new outcomes are derived from BaseException
|
It catches OutcomeException because of #2490 (issue #580), new outcomes
|
||||||
instead of Exception (for more details check #2707)
|
are derived from BaseException instead of Exception (for more details
|
||||||
|
check #2707).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return getattr(object, name, default)
|
return getattr(object, name, default)
|
||||||
|
@ -427,7 +423,7 @@ else:
|
||||||
#
|
#
|
||||||
# With `assert_never` we can do better:
|
# With `assert_never` we can do better:
|
||||||
#
|
#
|
||||||
# // throw new Error('unreachable');
|
# // raise Exception('unreachable')
|
||||||
# return assert_never(x)
|
# return assert_never(x)
|
||||||
#
|
#
|
||||||
# Now, if we forget to handle the new variant, the type-checker will emit a
|
# Now, if we forget to handle the new variant, the type-checker will emit a
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" command line options, ini-file and conftest.py processing. """
|
"""Command line options, ini-file and conftest.py processing."""
|
||||||
import argparse
|
import argparse
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import contextlib
|
import contextlib
|
||||||
|
@ -34,7 +34,7 @@ from pluggy import PluginManager
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import _pytest.deprecated
|
import _pytest.deprecated
|
||||||
import _pytest.hookspec # the extension point definitions
|
import _pytest.hookspec
|
||||||
from .exceptions import PrintHelp as PrintHelp
|
from .exceptions import PrintHelp as PrintHelp
|
||||||
from .exceptions import UsageError as UsageError
|
from .exceptions import UsageError as UsageError
|
||||||
from .findpaths import determine_setup
|
from .findpaths import determine_setup
|
||||||
|
@ -61,9 +61,12 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
_PluggyPlugin = object
|
_PluggyPlugin = object
|
||||||
"""A type to represent plugin objects.
|
"""A type to represent plugin objects.
|
||||||
|
|
||||||
Plugins can be any namespace, so we can't narrow it down much, but we use an
|
Plugins can be any namespace, so we can't narrow it down much, but we use an
|
||||||
alias to make the intent clear.
|
alias to make the intent clear.
|
||||||
Ideally this type would be provided by pluggy itself."""
|
|
||||||
|
Ideally this type would be provided by pluggy itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
hookimpl = HookimplMarker("pytest")
|
hookimpl = HookimplMarker("pytest")
|
||||||
|
@ -71,25 +74,24 @@ hookspec = HookspecMarker("pytest")
|
||||||
|
|
||||||
|
|
||||||
class ExitCode(enum.IntEnum):
|
class ExitCode(enum.IntEnum):
|
||||||
"""
|
"""Encodes the valid exit codes by pytest.
|
||||||
.. versionadded:: 5.0
|
|
||||||
|
|
||||||
Encodes the valid exit codes by pytest.
|
|
||||||
|
|
||||||
Currently users and plugins may supply other exit codes as well.
|
Currently users and plugins may supply other exit codes as well.
|
||||||
|
|
||||||
|
.. versionadded:: 5.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: tests passed
|
#: Tests passed.
|
||||||
OK = 0
|
OK = 0
|
||||||
#: tests failed
|
#: Tests failed.
|
||||||
TESTS_FAILED = 1
|
TESTS_FAILED = 1
|
||||||
#: pytest was interrupted
|
#: pytest was interrupted.
|
||||||
INTERRUPTED = 2
|
INTERRUPTED = 2
|
||||||
#: an internal error got in the way
|
#: An internal error got in the way.
|
||||||
INTERNAL_ERROR = 3
|
INTERNAL_ERROR = 3
|
||||||
#: pytest was misused
|
#: pytest was misused.
|
||||||
USAGE_ERROR = 4
|
USAGE_ERROR = 4
|
||||||
#: pytest couldn't find tests
|
#: pytest couldn't find tests.
|
||||||
NO_TESTS_COLLECTED = 5
|
NO_TESTS_COLLECTED = 5
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,7 +114,7 @@ class ConftestImportFailure(Exception):
|
||||||
def filter_traceback_for_conftest_import_failure(
|
def filter_traceback_for_conftest_import_failure(
|
||||||
entry: _pytest._code.TracebackEntry,
|
entry: _pytest._code.TracebackEntry,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""filters tracebacks entries which point to pytest internals or importlib.
|
"""Filter tracebacks entries which point to pytest internals or importlib.
|
||||||
|
|
||||||
Make a special case for importlib because we use it to import test modules and conftest files
|
Make a special case for importlib because we use it to import test modules and conftest files
|
||||||
in _pytest.pathlib.import_path.
|
in _pytest.pathlib.import_path.
|
||||||
|
@ -124,12 +126,12 @@ def main(
|
||||||
args: Optional[List[str]] = None,
|
args: Optional[List[str]] = None,
|
||||||
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
|
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
|
||||||
) -> Union[int, ExitCode]:
|
) -> Union[int, ExitCode]:
|
||||||
""" return exit code, after performing an in-process test run.
|
"""Perform an in-process test run.
|
||||||
|
|
||||||
:arg args: list of command line arguments.
|
:param args: List of command line arguments.
|
||||||
|
:param plugins: List of plugin objects to be auto-registered during initialization.
|
||||||
|
|
||||||
:arg plugins: list of plugin objects to be auto-registered during
|
:returns: An exit code.
|
||||||
initialization.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
@ -171,7 +173,7 @@ def main(
|
||||||
|
|
||||||
|
|
||||||
def console_main() -> int:
|
def console_main() -> int:
|
||||||
"""pytest's CLI entry point.
|
"""The CLI entry point of pytest.
|
||||||
|
|
||||||
This function is not meant for programmable use; use `main()` instead.
|
This function is not meant for programmable use; use `main()` instead.
|
||||||
"""
|
"""
|
||||||
|
@ -193,10 +195,10 @@ class cmdline: # compatibility namespace
|
||||||
|
|
||||||
|
|
||||||
def filename_arg(path: str, optname: str) -> str:
|
def filename_arg(path: str, optname: str) -> str:
|
||||||
""" Argparse type validator for filename arguments.
|
"""Argparse type validator for filename arguments.
|
||||||
|
|
||||||
:path: path of filename
|
:path: Path of filename.
|
||||||
:optname: name of the option
|
:optname: Name of the option.
|
||||||
"""
|
"""
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
raise UsageError("{} must be a filename, given: {}".format(optname, path))
|
raise UsageError("{} must be a filename, given: {}".format(optname, path))
|
||||||
|
@ -206,8 +208,8 @@ def filename_arg(path: str, optname: str) -> str:
|
||||||
def directory_arg(path: str, optname: str) -> str:
|
def directory_arg(path: str, optname: str) -> str:
|
||||||
"""Argparse type validator for directory arguments.
|
"""Argparse type validator for directory arguments.
|
||||||
|
|
||||||
:path: path of directory
|
:path: Path of directory.
|
||||||
:optname: name of the option
|
:optname: Name of the option.
|
||||||
"""
|
"""
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
raise UsageError("{} must be a directory, given: {}".format(optname, path))
|
raise UsageError("{} must be a directory, given: {}".format(optname, path))
|
||||||
|
@ -278,8 +280,7 @@ def get_config(
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_manager() -> "PytestPluginManager":
|
def get_plugin_manager() -> "PytestPluginManager":
|
||||||
"""
|
"""Obtain a new instance of the
|
||||||
Obtain a new instance of the
|
|
||||||
:py:class:`_pytest.config.PytestPluginManager`, with default plugins
|
:py:class:`_pytest.config.PytestPluginManager`, with default plugins
|
||||||
already loaded.
|
already loaded.
|
||||||
|
|
||||||
|
@ -320,13 +321,12 @@ def _prepareconfig(
|
||||||
|
|
||||||
|
|
||||||
class PytestPluginManager(PluginManager):
|
class PytestPluginManager(PluginManager):
|
||||||
"""
|
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
|
||||||
Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add pytest-specific
|
additional pytest-specific functionality:
|
||||||
functionality:
|
|
||||||
|
|
||||||
* loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
|
* Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
|
||||||
``pytest_plugins`` global variables found in plugins being loaded;
|
``pytest_plugins`` global variables found in plugins being loaded.
|
||||||
* ``conftest.py`` loading during start-up;
|
* ``conftest.py`` loading during start-up.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
@ -359,27 +359,27 @@ class PytestPluginManager(PluginManager):
|
||||||
|
|
||||||
# Config._consider_importhook will set a real object if required.
|
# Config._consider_importhook will set a real object if required.
|
||||||
self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
|
self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
|
||||||
# Used to know when we are importing conftests after the pytest_configure stage
|
# Used to know when we are importing conftests after the pytest_configure stage.
|
||||||
self._configured = False
|
self._configured = False
|
||||||
|
|
||||||
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
|
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
|
||||||
# pytest hooks are always prefixed with pytest_
|
# pytest hooks are always prefixed with "pytest_",
|
||||||
# so we avoid accessing possibly non-readable attributes
|
# so we avoid accessing possibly non-readable attributes
|
||||||
# (see issue #1073)
|
# (see issue #1073).
|
||||||
if not name.startswith("pytest_"):
|
if not name.startswith("pytest_"):
|
||||||
return
|
return
|
||||||
# ignore names which can not be hooks
|
# Ignore names which can not be hooks.
|
||||||
if name == "pytest_plugins":
|
if name == "pytest_plugins":
|
||||||
return
|
return
|
||||||
|
|
||||||
method = getattr(plugin, name)
|
method = getattr(plugin, name)
|
||||||
opts = super().parse_hookimpl_opts(plugin, name)
|
opts = super().parse_hookimpl_opts(plugin, name)
|
||||||
|
|
||||||
# consider only actual functions for hooks (#3775)
|
# Consider only actual functions for hooks (#3775).
|
||||||
if not inspect.isroutine(method):
|
if not inspect.isroutine(method):
|
||||||
return
|
return
|
||||||
|
|
||||||
# collect unmarked hooks as long as they have the `pytest_' prefix
|
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
||||||
if opts is None and name.startswith("pytest_"):
|
if opts is None and name.startswith("pytest_"):
|
||||||
opts = {}
|
opts = {}
|
||||||
if opts is not None:
|
if opts is not None:
|
||||||
|
@ -432,17 +432,18 @@ class PytestPluginManager(PluginManager):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def getplugin(self, name: str):
|
def getplugin(self, name: str):
|
||||||
# support deprecated naming because plugins (xdist e.g.) use it
|
# Support deprecated naming because plugins (xdist e.g.) use it.
|
||||||
plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin]
|
plugin = self.get_plugin(name) # type: Optional[_PluggyPlugin]
|
||||||
return plugin
|
return plugin
|
||||||
|
|
||||||
def hasplugin(self, name: str) -> bool:
|
def hasplugin(self, name: str) -> bool:
|
||||||
"""Return True if the plugin with the given name is registered."""
|
"""Return whether a plugin with the given name is registered."""
|
||||||
return bool(self.get_plugin(name))
|
return bool(self.get_plugin(name))
|
||||||
|
|
||||||
def pytest_configure(self, config: "Config") -> None:
|
def pytest_configure(self, config: "Config") -> None:
|
||||||
|
""":meta private:"""
|
||||||
# XXX now that the pluginmanager exposes hookimpl(tryfirst...)
|
# XXX now that the pluginmanager exposes hookimpl(tryfirst...)
|
||||||
# we should remove tryfirst/trylast as markers
|
# we should remove tryfirst/trylast as markers.
|
||||||
config.addinivalue_line(
|
config.addinivalue_line(
|
||||||
"markers",
|
"markers",
|
||||||
"tryfirst: mark a hook implementation function such that the "
|
"tryfirst: mark a hook implementation function such that the "
|
||||||
|
@ -456,15 +457,15 @@ class PytestPluginManager(PluginManager):
|
||||||
self._configured = True
|
self._configured = True
|
||||||
|
|
||||||
#
|
#
|
||||||
# internal API for local conftest plugin handling
|
# Internal API for local conftest plugin handling.
|
||||||
#
|
#
|
||||||
def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
|
def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
|
||||||
""" load initial conftest files given a preparsed "namespace".
|
"""Load initial conftest files given a preparsed "namespace".
|
||||||
As conftest files may add their own command line options
|
|
||||||
which have arguments ('--my-opt somepath') we might get some
|
As conftest files may add their own command line options which have
|
||||||
false positives. All builtin and 3rd party plugins will have
|
arguments ('--my-opt somepath') we might get some false positives.
|
||||||
been loaded, however, so common options will not confuse our logic
|
All builtin and 3rd party plugins will have been loaded, however, so
|
||||||
here.
|
common options will not confuse our logic here.
|
||||||
"""
|
"""
|
||||||
current = py.path.local()
|
current = py.path.local()
|
||||||
self._confcutdir = (
|
self._confcutdir = (
|
||||||
|
@ -513,7 +514,7 @@ class PytestPluginManager(PluginManager):
|
||||||
|
|
||||||
# XXX these days we may rather want to use config.rootdir
|
# XXX these days we may rather want to use config.rootdir
|
||||||
# and allow users to opt into looking into the rootdir parent
|
# and allow users to opt into looking into the rootdir parent
|
||||||
# directories instead of requiring to specify confcutdir
|
# directories instead of requiring to specify confcutdir.
|
||||||
clist = []
|
clist = []
|
||||||
for parent in directory.parts():
|
for parent in directory.parts():
|
||||||
if self._confcutdir and self._confcutdir.relto(parent):
|
if self._confcutdir and self._confcutdir.relto(parent):
|
||||||
|
@ -539,8 +540,8 @@ class PytestPluginManager(PluginManager):
|
||||||
def _importconftest(
|
def _importconftest(
|
||||||
self, conftestpath: py.path.local, importmode: Union[str, ImportMode],
|
self, conftestpath: py.path.local, importmode: Union[str, ImportMode],
|
||||||
) -> types.ModuleType:
|
) -> types.ModuleType:
|
||||||
# Use a resolved Path object as key to avoid loading the same conftest twice
|
# Use a resolved Path object as key to avoid loading the same conftest
|
||||||
# with build systems that create build directories containing
|
# twice with build systems that create build directories containing
|
||||||
# symlinks to actual files.
|
# symlinks to actual files.
|
||||||
# Using Path().resolve() is better than py.path.realpath because
|
# Using Path().resolve() is better than py.path.realpath because
|
||||||
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
|
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
|
||||||
|
@ -627,7 +628,7 @@ class PytestPluginManager(PluginManager):
|
||||||
if name in essential_plugins:
|
if name in essential_plugins:
|
||||||
raise UsageError("plugin %s cannot be disabled" % name)
|
raise UsageError("plugin %s cannot be disabled" % name)
|
||||||
|
|
||||||
# PR #4304 : remove stepwise if cacheprovider is blocked
|
# PR #4304: remove stepwise if cacheprovider is blocked.
|
||||||
if name == "cacheprovider":
|
if name == "cacheprovider":
|
||||||
self.set_blocked("stepwise")
|
self.set_blocked("stepwise")
|
||||||
self.set_blocked("pytest_stepwise")
|
self.set_blocked("pytest_stepwise")
|
||||||
|
@ -663,11 +664,12 @@ class PytestPluginManager(PluginManager):
|
||||||
self.import_plugin(import_spec)
|
self.import_plugin(import_spec)
|
||||||
|
|
||||||
def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
|
def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
|
||||||
|
"""Import a plugin with ``modname``.
|
||||||
|
|
||||||
|
If ``consider_entry_points`` is True, entry point names are also
|
||||||
|
considered to find a plugin.
|
||||||
"""
|
"""
|
||||||
Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point
|
# Most often modname refers to builtin modules, e.g. "pytester",
|
||||||
names are also considered to find a plugin.
|
|
||||||
"""
|
|
||||||
# most often modname refers to builtin modules, e.g. "pytester",
|
|
||||||
# "terminal" or "capture". Those plugins are registered under their
|
# "terminal" or "capture". Those plugins are registered under their
|
||||||
# basename for historic purposes but must be imported with the
|
# basename for historic purposes but must be imported with the
|
||||||
# _pytest prefix.
|
# _pytest prefix.
|
||||||
|
@ -743,10 +745,11 @@ notset = Notset()
|
||||||
|
|
||||||
|
|
||||||
def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
|
def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
|
||||||
"""
|
"""Given an iterable of file names in a source distribution, return the "names" that should
|
||||||
Given an iterable of file names in a source distribution, return the "names" that should
|
be marked for assertion rewrite.
|
||||||
be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should
|
|
||||||
be added as "pytest_mock" in the assertion rewrite mechanism.
|
For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
|
||||||
|
the assertion rewrite mechanism.
|
||||||
|
|
||||||
This function has to deal with dist-info based distributions and egg based distributions
|
This function has to deal with dist-info based distributions and egg based distributions
|
||||||
(which are still very much in use for "editable" installs).
|
(which are still very much in use for "editable" installs).
|
||||||
|
@ -790,11 +793,11 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
|
||||||
yield package_name
|
yield package_name
|
||||||
|
|
||||||
if not seen_some:
|
if not seen_some:
|
||||||
# at this point we did not find any packages or modules suitable for assertion
|
# At this point we did not find any packages or modules suitable for assertion
|
||||||
# rewriting, so we try again by stripping the first path component (to account for
|
# rewriting, so we try again by stripping the first path component (to account for
|
||||||
# "src" based source trees for example)
|
# "src" based source trees for example).
|
||||||
# this approach lets us have the common case continue to be fast, as egg-distributions
|
# This approach lets us have the common case continue to be fast, as egg-distributions
|
||||||
# are rarer
|
# are rarer.
|
||||||
new_package_files = []
|
new_package_files = []
|
||||||
for fn in package_files:
|
for fn in package_files:
|
||||||
parts = fn.split("/")
|
parts = fn.split("/")
|
||||||
|
@ -810,8 +813,7 @@ def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""
|
"""Access to configuration values, pluginmanager and plugin hooks.
|
||||||
Access to configuration values, pluginmanager and plugin hooks.
|
|
||||||
|
|
||||||
:param PytestPluginManager pluginmanager:
|
:param PytestPluginManager pluginmanager:
|
||||||
|
|
||||||
|
@ -837,11 +839,11 @@ class Config:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
|
args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
|
||||||
"""tuple of command-line arguments as passed to ``pytest.main()``."""
|
"""Tuple of command-line arguments as passed to ``pytest.main()``."""
|
||||||
plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
|
plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
|
||||||
"""list of extra plugins, might be `None`."""
|
"""List of extra plugins, might be `None`."""
|
||||||
dir = attr.ib(type=Path)
|
dir = attr.ib(type=Path)
|
||||||
"""directory where ``pytest.main()`` was invoked from."""
|
"""Directory from which ``pytest.main()`` was invoked."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -857,9 +859,10 @@ class Config:
|
||||||
)
|
)
|
||||||
|
|
||||||
self.option = argparse.Namespace()
|
self.option = argparse.Namespace()
|
||||||
"""access to command line option as attributes.
|
"""Access to command line option as attributes.
|
||||||
|
|
||||||
:type: argparse.Namespace"""
|
:type: argparse.Namespace
|
||||||
|
"""
|
||||||
|
|
||||||
self.invocation_params = invocation_params
|
self.invocation_params = invocation_params
|
||||||
|
|
||||||
|
@ -869,9 +872,10 @@ class Config:
|
||||||
processopt=self._processopt,
|
processopt=self._processopt,
|
||||||
)
|
)
|
||||||
self.pluginmanager = pluginmanager
|
self.pluginmanager = pluginmanager
|
||||||
"""the plugin manager handles plugin registration and hook invocation.
|
"""The plugin manager handles plugin registration and hook invocation.
|
||||||
|
|
||||||
:type: PytestPluginManager"""
|
:type: PytestPluginManager.
|
||||||
|
"""
|
||||||
|
|
||||||
self.trace = self.pluginmanager.trace.root.get("config")
|
self.trace = self.pluginmanager.trace.root.get("config")
|
||||||
self.hook = self.pluginmanager.hook
|
self.hook = self.pluginmanager.hook
|
||||||
|
@ -895,11 +899,11 @@ class Config:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invocation_dir(self) -> py.path.local:
|
def invocation_dir(self) -> py.path.local:
|
||||||
"""Backward compatibility"""
|
"""Backward compatibility."""
|
||||||
return py.path.local(str(self.invocation_params.dir))
|
return py.path.local(str(self.invocation_params.dir))
|
||||||
|
|
||||||
def add_cleanup(self, func: Callable[[], None]) -> None:
|
def add_cleanup(self, func: Callable[[], None]) -> None:
|
||||||
""" Add a function to be called when the config object gets out of
|
"""Add a function to be called when the config object gets out of
|
||||||
use (usually coninciding with pytest_unconfigure)."""
|
use (usually coninciding with pytest_unconfigure)."""
|
||||||
self._cleanup.append(func)
|
self._cleanup.append(func)
|
||||||
|
|
||||||
|
@ -970,7 +974,7 @@ class Config:
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|
||||||
def cwd_relative_nodeid(self, nodeid: str) -> str:
|
def cwd_relative_nodeid(self, nodeid: str) -> str:
|
||||||
# nodeid's are relative to the rootpath, compute relative to cwd
|
# nodeid's are relative to the rootpath, compute relative to cwd.
|
||||||
if self.invocation_dir != self.rootdir:
|
if self.invocation_dir != self.rootdir:
|
||||||
fullpath = self.rootdir.join(nodeid)
|
fullpath = self.rootdir.join(nodeid)
|
||||||
nodeid = self.invocation_dir.bestrelpath(fullpath)
|
nodeid = self.invocation_dir.bestrelpath(fullpath)
|
||||||
|
@ -978,7 +982,7 @@ class Config:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromdictargs(cls, option_dict, args) -> "Config":
|
def fromdictargs(cls, option_dict, args) -> "Config":
|
||||||
""" constructor usable for subprocesses. """
|
"""Constructor usable for subprocesses."""
|
||||||
config = get_config(args)
|
config = get_config(args)
|
||||||
config.option.__dict__.update(option_dict)
|
config.option.__dict__.update(option_dict)
|
||||||
config.parse(args, addopts=False)
|
config.parse(args, addopts=False)
|
||||||
|
@ -1041,11 +1045,9 @@ class Config:
|
||||||
self._warn_about_missing_assertion(mode)
|
self._warn_about_missing_assertion(mode)
|
||||||
|
|
||||||
def _mark_plugins_for_rewrite(self, hook) -> None:
|
def _mark_plugins_for_rewrite(self, hook) -> None:
|
||||||
"""
|
"""Given an importhook, mark for rewrite any top-level
|
||||||
Given an importhook, mark for rewrite any top-level
|
|
||||||
modules or packages in the distribution package for
|
modules or packages in the distribution package for
|
||||||
all pytest plugins.
|
all pytest plugins."""
|
||||||
"""
|
|
||||||
self.pluginmanager.rewrite_hook = hook
|
self.pluginmanager.rewrite_hook = hook
|
||||||
|
|
||||||
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||||
|
@ -1194,7 +1196,7 @@ class Config:
|
||||||
return [name for name in self.inicfg if name not in parser_inicfg]
|
return [name for name in self.inicfg if name not in parser_inicfg]
|
||||||
|
|
||||||
def parse(self, args: List[str], addopts: bool = True) -> None:
|
def parse(self, args: List[str], addopts: bool = True) -> None:
|
||||||
# parse given cmdline arguments into this config object.
|
# Parse given cmdline arguments into this config object.
|
||||||
assert not hasattr(
|
assert not hasattr(
|
||||||
self, "args"
|
self, "args"
|
||||||
), "can only parse cmdline args at most once per Config object"
|
), "can only parse cmdline args at most once per Config object"
|
||||||
|
@ -1219,18 +1221,20 @@ class Config:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def addinivalue_line(self, name: str, line: str) -> None:
|
def addinivalue_line(self, name: str, line: str) -> None:
|
||||||
""" add a line to an ini-file option. The option must have been
|
"""Add a line to an ini-file option. The option must have been
|
||||||
declared but might not yet be set in which case the line becomes the
|
declared but might not yet be set in which case the line becomes
|
||||||
the first line in its value. """
|
the first line in its value."""
|
||||||
x = self.getini(name)
|
x = self.getini(name)
|
||||||
assert isinstance(x, list)
|
assert isinstance(x, list)
|
||||||
x.append(line) # modifies the cached list inline
|
x.append(line) # modifies the cached list inline
|
||||||
|
|
||||||
def getini(self, name: str):
|
def getini(self, name: str):
|
||||||
""" return configuration value from an :ref:`ini file <configfiles>`. If the
|
"""Return configuration value from an :ref:`ini file <configfiles>`.
|
||||||
specified name hasn't been registered through a prior
|
|
||||||
|
If the specified name hasn't been registered through a prior
|
||||||
:py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>`
|
:py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>`
|
||||||
call (usually from a plugin), a ValueError is raised. """
|
call (usually from a plugin), a ValueError is raised.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self._inicache[name]
|
return self._inicache[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -1254,19 +1258,20 @@ class Config:
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
value = override_value
|
value = override_value
|
||||||
# coerce the values based on types
|
# Coerce the values based on types.
|
||||||
# note: some coercions are only required if we are reading from .ini files, because
|
#
|
||||||
|
# Note: some coercions are only required if we are reading from .ini files, because
|
||||||
# the file format doesn't contain type information, but when reading from toml we will
|
# the file format doesn't contain type information, but when reading from toml we will
|
||||||
# get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
|
# get either str or list of str values (see _parse_ini_config_from_pyproject_toml).
|
||||||
# for example:
|
# For example:
|
||||||
#
|
#
|
||||||
# ini:
|
# ini:
|
||||||
# a_line_list = "tests acceptance"
|
# a_line_list = "tests acceptance"
|
||||||
# in this case, we need to split the string to obtain a list of strings
|
# in this case, we need to split the string to obtain a list of strings.
|
||||||
#
|
#
|
||||||
# toml:
|
# toml:
|
||||||
# a_line_list = ["tests", "acceptance"]
|
# a_line_list = ["tests", "acceptance"]
|
||||||
# in this case, we already have a list ready to use
|
# in this case, we already have a list ready to use.
|
||||||
#
|
#
|
||||||
if type == "pathlist":
|
if type == "pathlist":
|
||||||
# TODO: This assert is probably not valid in all cases.
|
# TODO: This assert is probably not valid in all cases.
|
||||||
|
@ -1307,9 +1312,9 @@ class Config:
|
||||||
|
|
||||||
def _get_override_ini_value(self, name: str) -> Optional[str]:
|
def _get_override_ini_value(self, name: str) -> Optional[str]:
|
||||||
value = None
|
value = None
|
||||||
# override_ini is a list of "ini=value" options
|
# override_ini is a list of "ini=value" options.
|
||||||
# always use the last item if multiple values are set for same ini-name,
|
# Always use the last item if multiple values are set for same ini-name,
|
||||||
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
|
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
|
||||||
for ini_config in self._override_ini:
|
for ini_config in self._override_ini:
|
||||||
try:
|
try:
|
||||||
key, user_ini_value = ini_config.split("=", 1)
|
key, user_ini_value = ini_config.split("=", 1)
|
||||||
|
@ -1325,12 +1330,12 @@ class Config:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def getoption(self, name: str, default=notset, skip: bool = False):
|
def getoption(self, name: str, default=notset, skip: bool = False):
|
||||||
""" return command line option value.
|
"""Return command line option value.
|
||||||
|
|
||||||
:arg name: name of the option. You may also specify
|
:param name: Name of the option. You may also specify
|
||||||
the literal ``--OPT`` option instead of the "dest" option name.
|
the literal ``--OPT`` option instead of the "dest" option name.
|
||||||
:arg default: default value if no option of that name exists.
|
:param default: Default value if no option of that name exists.
|
||||||
:arg skip: if True raise pytest.skip if option does not exists
|
:param skip: If True, raise pytest.skip if option does not exists
|
||||||
or has a None value.
|
or has a None value.
|
||||||
"""
|
"""
|
||||||
name = self._opt2dest.get(name, name)
|
name = self._opt2dest.get(name, name)
|
||||||
|
@ -1349,11 +1354,11 @@ class Config:
|
||||||
raise ValueError("no option named {!r}".format(name)) from e
|
raise ValueError("no option named {!r}".format(name)) from e
|
||||||
|
|
||||||
def getvalue(self, name: str, path=None):
|
def getvalue(self, name: str, path=None):
|
||||||
""" (deprecated, use getoption()) """
|
"""Deprecated, use getoption() instead."""
|
||||||
return self.getoption(name)
|
return self.getoption(name)
|
||||||
|
|
||||||
def getvalueorskip(self, name: str, path=None):
|
def getvalueorskip(self, name: str, path=None):
|
||||||
""" (deprecated, use getoption(skip=True)) """
|
"""Deprecated, use getoption(skip=True) instead."""
|
||||||
return self.getoption(name, skip=True)
|
return self.getoption(name, skip=True)
|
||||||
|
|
||||||
def _warn_about_missing_assertion(self, mode: str) -> None:
|
def _warn_about_missing_assertion(self, mode: str) -> None:
|
||||||
|
@ -1392,10 +1397,13 @@ def create_terminal_writer(
|
||||||
config: Config, file: Optional[TextIO] = None
|
config: Config, file: Optional[TextIO] = None
|
||||||
) -> TerminalWriter:
|
) -> TerminalWriter:
|
||||||
"""Create a TerminalWriter instance configured according to the options
|
"""Create a TerminalWriter instance configured according to the options
|
||||||
in the config object. Every code which requires a TerminalWriter object
|
in the config object.
|
||||||
and has access to a config object should use this function.
|
|
||||||
|
Every code which requires a TerminalWriter object and has access to a
|
||||||
|
config object should use this function.
|
||||||
"""
|
"""
|
||||||
tw = TerminalWriter(file=file)
|
tw = TerminalWriter(file=file)
|
||||||
|
|
||||||
if config.option.color == "yes":
|
if config.option.color == "yes":
|
||||||
tw.hasmarkup = True
|
tw.hasmarkup = True
|
||||||
elif config.option.color == "no":
|
elif config.option.color == "no":
|
||||||
|
@ -1405,6 +1413,7 @@ def create_terminal_writer(
|
||||||
tw.code_highlight = True
|
tw.code_highlight = True
|
||||||
elif config.option.code_highlight == "no":
|
elif config.option.code_highlight == "no":
|
||||||
tw.code_highlight = False
|
tw.code_highlight = False
|
||||||
|
|
||||||
return tw
|
return tw
|
||||||
|
|
||||||
|
|
||||||
|
@ -1415,7 +1424,7 @@ def _strtobool(val: str) -> bool:
|
||||||
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
|
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
|
||||||
'val' is anything else.
|
'val' is anything else.
|
||||||
|
|
||||||
.. note:: copied from distutils.util
|
.. note:: Copied from distutils.util.
|
||||||
"""
|
"""
|
||||||
val = val.lower()
|
val = val.lower()
|
||||||
if val in ("y", "yes", "t", "true", "on", "1"):
|
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||||
|
|
|
@ -27,9 +27,9 @@ FILE_OR_DIR = "file_or_dir"
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
""" Parser for command line arguments and ini-file values.
|
"""Parser for command line arguments and ini-file values.
|
||||||
|
|
||||||
:ivar extra_info: dict of generic param -> value to display in case
|
:ivar extra_info: Dict of generic param -> value to display in case
|
||||||
there's an error processing the command line arguments.
|
there's an error processing the command line arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -56,11 +56,11 @@ class Parser:
|
||||||
def getgroup(
|
def getgroup(
|
||||||
self, name: str, description: str = "", after: Optional[str] = None
|
self, name: str, description: str = "", after: Optional[str] = None
|
||||||
) -> "OptionGroup":
|
) -> "OptionGroup":
|
||||||
""" get (or create) a named option Group.
|
"""Get (or create) a named option Group.
|
||||||
|
|
||||||
:name: name of the option group.
|
:name: Name of the option group.
|
||||||
:description: long description for --help output.
|
:description: Long description for --help output.
|
||||||
:after: name of other group, used for ordering --help output.
|
:after: Name of another group, used for ordering --help output.
|
||||||
|
|
||||||
The returned group object has an ``addoption`` method with the same
|
The returned group object has an ``addoption`` method with the same
|
||||||
signature as :py:func:`parser.addoption
|
signature as :py:func:`parser.addoption
|
||||||
|
@ -79,15 +79,14 @@ class Parser:
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def addoption(self, *opts: str, **attrs: Any) -> None:
|
def addoption(self, *opts: str, **attrs: Any) -> None:
|
||||||
""" register a command line option.
|
"""Register a command line option.
|
||||||
|
|
||||||
:opts: option names, can be short or long options.
|
:opts: Option names, can be short or long options.
|
||||||
:attrs: same attributes which the ``add_argument()`` function of the
|
:attrs: Same attributes which the ``add_argument()`` function of the
|
||||||
`argparse library
|
`argparse library <https://docs.python.org/library/argparse.html>`_
|
||||||
<https://docs.python.org/library/argparse.html>`_
|
|
||||||
accepts.
|
accepts.
|
||||||
|
|
||||||
After command line parsing options are available on the pytest config
|
After command line parsing, options are available on the pytest config
|
||||||
object via ``config.option.NAME`` where ``NAME`` is usually set
|
object via ``config.option.NAME`` where ``NAME`` is usually set
|
||||||
by passing a ``dest`` attribute, for example
|
by passing a ``dest`` attribute, for example
|
||||||
``addoption("--long", dest="NAME", ...)``.
|
``addoption("--long", dest="NAME", ...)``.
|
||||||
|
@ -141,9 +140,7 @@ class Parser:
|
||||||
args: Sequence[Union[str, py.path.local]],
|
args: Sequence[Union[str, py.path.local]],
|
||||||
namespace: Optional[argparse.Namespace] = None,
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
) -> argparse.Namespace:
|
) -> argparse.Namespace:
|
||||||
"""parses and returns a namespace object with known arguments at this
|
"""Parse and return a namespace object with known arguments at this point."""
|
||||||
point.
|
|
||||||
"""
|
|
||||||
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
|
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
|
||||||
|
|
||||||
def parse_known_and_unknown_args(
|
def parse_known_and_unknown_args(
|
||||||
|
@ -151,9 +148,8 @@ class Parser:
|
||||||
args: Sequence[Union[str, py.path.local]],
|
args: Sequence[Union[str, py.path.local]],
|
||||||
namespace: Optional[argparse.Namespace] = None,
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
) -> Tuple[argparse.Namespace, List[str]]:
|
) -> Tuple[argparse.Namespace, List[str]]:
|
||||||
"""parses and returns a namespace object with known arguments, and
|
"""Parse and return a namespace object with known arguments, and
|
||||||
the remaining arguments unknown at this point.
|
the remaining arguments unknown at this point."""
|
||||||
"""
|
|
||||||
optparser = self._getparser()
|
optparser = self._getparser()
|
||||||
strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
strargs = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||||
return optparser.parse_known_args(strargs, namespace=namespace)
|
return optparser.parse_known_args(strargs, namespace=namespace)
|
||||||
|
@ -165,12 +161,12 @@ class Parser:
|
||||||
type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
|
type: Optional["Literal['pathlist', 'args', 'linelist', 'bool']"] = None,
|
||||||
default=None,
|
default=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
""" register an ini-file option.
|
"""Register an ini-file option.
|
||||||
|
|
||||||
:name: name of the ini-variable
|
:name: Name of the ini-variable.
|
||||||
:type: type of the variable, can be ``pathlist``, ``args``, ``linelist``
|
:type: Type of the variable, can be ``pathlist``, ``args``, ``linelist``
|
||||||
or ``bool``.
|
or ``bool``.
|
||||||
:default: default value if no ini-file option exists but is queried.
|
:default: Default value if no ini-file option exists but is queried.
|
||||||
|
|
||||||
The value of ini-variables can be retrieved via a call to
|
The value of ini-variables can be retrieved via a call to
|
||||||
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
|
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
|
||||||
|
@ -181,10 +177,8 @@ class Parser:
|
||||||
|
|
||||||
|
|
||||||
class ArgumentError(Exception):
|
class ArgumentError(Exception):
|
||||||
"""
|
"""Raised if an Argument instance is created with invalid or
|
||||||
Raised if an Argument instance is created with invalid or
|
inconsistent arguments."""
|
||||||
inconsistent arguments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, msg: str, option: Union["Argument", str]) -> None:
|
def __init__(self, msg: str, option: Union["Argument", str]) -> None:
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
@ -198,17 +192,18 @@ class ArgumentError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class Argument:
|
class Argument:
|
||||||
"""class that mimics the necessary behaviour of optparse.Option
|
"""Class that mimics the necessary behaviour of optparse.Option.
|
||||||
|
|
||||||
|
It's currently a least effort implementation and ignoring choices
|
||||||
|
and integer prefixes.
|
||||||
|
|
||||||
it's currently a least effort implementation
|
|
||||||
and ignoring choices and integer prefixes
|
|
||||||
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
|
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
||||||
|
|
||||||
def __init__(self, *names: str, **attrs: Any) -> None:
|
def __init__(self, *names: str, **attrs: Any) -> None:
|
||||||
"""store parms in private vars for use in add_argument"""
|
"""Store parms in private vars for use in add_argument."""
|
||||||
self._attrs = attrs
|
self._attrs = attrs
|
||||||
self._short_opts = [] # type: List[str]
|
self._short_opts = [] # type: List[str]
|
||||||
self._long_opts = [] # type: List[str]
|
self._long_opts = [] # type: List[str]
|
||||||
|
@ -224,7 +219,7 @@ class Argument:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# this might raise a keyerror as well, don't want to catch that
|
# This might raise a keyerror as well, don't want to catch that.
|
||||||
if isinstance(typ, str):
|
if isinstance(typ, str):
|
||||||
if typ == "choice":
|
if typ == "choice":
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
@ -247,12 +242,12 @@ class Argument:
|
||||||
stacklevel=4,
|
stacklevel=4,
|
||||||
)
|
)
|
||||||
attrs["type"] = Argument._typ_map[typ]
|
attrs["type"] = Argument._typ_map[typ]
|
||||||
# used in test_parseopt -> test_parse_defaultgetter
|
# Used in test_parseopt -> test_parse_defaultgetter.
|
||||||
self.type = attrs["type"]
|
self.type = attrs["type"]
|
||||||
else:
|
else:
|
||||||
self.type = typ
|
self.type = typ
|
||||||
try:
|
try:
|
||||||
# attribute existence is tested in Config._processopt
|
# Attribute existence is tested in Config._processopt.
|
||||||
self.default = attrs["default"]
|
self.default = attrs["default"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
@ -273,7 +268,7 @@ class Argument:
|
||||||
return self._short_opts + self._long_opts
|
return self._short_opts + self._long_opts
|
||||||
|
|
||||||
def attrs(self) -> Mapping[str, Any]:
|
def attrs(self) -> Mapping[str, Any]:
|
||||||
# update any attributes set by processopt
|
# Update any attributes set by processopt.
|
||||||
attrs = "default dest help".split()
|
attrs = "default dest help".split()
|
||||||
attrs.append(self.dest)
|
attrs.append(self.dest)
|
||||||
for attr in attrs:
|
for attr in attrs:
|
||||||
|
@ -289,9 +284,10 @@ class Argument:
|
||||||
return self._attrs
|
return self._attrs
|
||||||
|
|
||||||
def _set_opt_strings(self, opts: Sequence[str]) -> None:
|
def _set_opt_strings(self, opts: Sequence[str]) -> None:
|
||||||
"""directly from optparse
|
"""Directly from optparse.
|
||||||
|
|
||||||
might not be necessary as this is passed to argparse later on"""
|
Might not be necessary as this is passed to argparse later on.
|
||||||
|
"""
|
||||||
for opt in opts:
|
for opt in opts:
|
||||||
if len(opt) < 2:
|
if len(opt) < 2:
|
||||||
raise ArgumentError(
|
raise ArgumentError(
|
||||||
|
@ -340,12 +336,12 @@ class OptionGroup:
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
|
||||||
def addoption(self, *optnames: str, **attrs: Any) -> None:
|
def addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||||
""" add an option to this group.
|
"""Add an option to this group.
|
||||||
|
|
||||||
if a shortened version of a long option is specified it will
|
If a shortened version of a long option is specified, it will
|
||||||
be suppressed in the help. addoption('--twowords', '--two-words')
|
be suppressed in the help. addoption('--twowords', '--two-words')
|
||||||
results in help showing '--two-words' only, but --twowords gets
|
results in help showing '--two-words' only, but --twowords gets
|
||||||
accepted **and** the automatic destination is in args.twowords
|
accepted **and** the automatic destination is in args.twowords.
|
||||||
"""
|
"""
|
||||||
conflict = set(optnames).intersection(
|
conflict = set(optnames).intersection(
|
||||||
name for opt in self.options for name in opt.names()
|
name for opt in self.options for name in opt.names()
|
||||||
|
@ -386,7 +382,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
allow_abbrev=False,
|
allow_abbrev=False,
|
||||||
)
|
)
|
||||||
# extra_info is a dict of (param -> value) to display if there's
|
# extra_info is a dict of (param -> value) to display if there's
|
||||||
# an usage error to provide more contextual information to the user
|
# an usage error to provide more contextual information to the user.
|
||||||
self.extra_info = extra_info if extra_info else {}
|
self.extra_info = extra_info if extra_info else {}
|
||||||
|
|
||||||
def error(self, message: str) -> "NoReturn":
|
def error(self, message: str) -> "NoReturn":
|
||||||
|
@ -405,7 +401,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
args: Optional[Sequence[str]] = None,
|
args: Optional[Sequence[str]] = None,
|
||||||
namespace: Optional[argparse.Namespace] = None,
|
namespace: Optional[argparse.Namespace] = None,
|
||||||
) -> argparse.Namespace:
|
) -> argparse.Namespace:
|
||||||
"""allow splitting of positional arguments"""
|
"""Allow splitting of positional arguments."""
|
||||||
parsed, unrecognized = self.parse_known_args(args, namespace)
|
parsed, unrecognized = self.parse_known_args(args, namespace)
|
||||||
if unrecognized:
|
if unrecognized:
|
||||||
for arg in unrecognized:
|
for arg in unrecognized:
|
||||||
|
@ -457,15 +453,15 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
|
|
||||||
|
|
||||||
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||||
"""shorten help for long options that differ only in extra hyphens
|
"""Shorten help for long options that differ only in extra hyphens.
|
||||||
|
|
||||||
- collapse **long** options that are the same except for extra hyphens
|
- Collapse **long** options that are the same except for extra hyphens.
|
||||||
- shortcut if there are only two options and one of them is a short one
|
- Shortcut if there are only two options and one of them is a short one.
|
||||||
- cache result on action object as this is called at least 2 times
|
- Cache result on the action object as this is called at least 2 times.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""Use more accurate terminal width via pylib."""
|
# Use more accurate terminal width.
|
||||||
if "width" not in kwargs:
|
if "width" not in kwargs:
|
||||||
kwargs["width"] = _pytest._io.get_terminal_width()
|
kwargs["width"] = _pytest._io.get_terminal_width()
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
class UsageError(Exception):
|
class UsageError(Exception):
|
||||||
""" error in pytest usage or invocation"""
|
"""Error in pytest usage or invocation."""
|
||||||
|
|
||||||
|
|
||||||
class PrintHelp(Exception):
|
class PrintHelp(Exception):
|
||||||
"""Raised when pytest should print it's help to skip the rest of the
|
"""Raised when pytest should print its help to skip the rest of the
|
||||||
argument parsing and validation."""
|
argument parsing and validation."""
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
|
@ -18,10 +18,10 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
|
def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
|
||||||
"""Parses the given generic '.ini' file using legacy IniConfig parser, returning
|
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning
|
||||||
the parsed object.
|
the parsed object.
|
||||||
|
|
||||||
Raises UsageError if the file cannot be parsed.
|
Raise UsageError if the file cannot be parsed.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return iniconfig.IniConfig(path)
|
return iniconfig.IniConfig(path)
|
||||||
|
@ -32,23 +32,23 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
|
||||||
def load_config_dict_from_file(
|
def load_config_dict_from_file(
|
||||||
filepath: py.path.local,
|
filepath: py.path.local,
|
||||||
) -> Optional[Dict[str, Union[str, List[str]]]]:
|
) -> Optional[Dict[str, Union[str, List[str]]]]:
|
||||||
"""Loads pytest configuration from the given file path, if supported.
|
"""Load pytest configuration from the given file path, if supported.
|
||||||
|
|
||||||
Return None if the file does not contain valid pytest configuration.
|
Return None if the file does not contain valid pytest configuration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# configuration from ini files are obtained from the [pytest] section, if present.
|
# Configuration from ini files are obtained from the [pytest] section, if present.
|
||||||
if filepath.ext == ".ini":
|
if filepath.ext == ".ini":
|
||||||
iniconfig = _parse_ini_config(filepath)
|
iniconfig = _parse_ini_config(filepath)
|
||||||
|
|
||||||
if "pytest" in iniconfig:
|
if "pytest" in iniconfig:
|
||||||
return dict(iniconfig["pytest"].items())
|
return dict(iniconfig["pytest"].items())
|
||||||
else:
|
else:
|
||||||
# "pytest.ini" files are always the source of configuration, even if empty
|
# "pytest.ini" files are always the source of configuration, even if empty.
|
||||||
if filepath.basename == "pytest.ini":
|
if filepath.basename == "pytest.ini":
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# '.cfg' files are considered if they contain a "[tool:pytest]" section
|
# '.cfg' files are considered if they contain a "[tool:pytest]" section.
|
||||||
elif filepath.ext == ".cfg":
|
elif filepath.ext == ".cfg":
|
||||||
iniconfig = _parse_ini_config(filepath)
|
iniconfig = _parse_ini_config(filepath)
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ def load_config_dict_from_file(
|
||||||
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
|
# plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
|
||||||
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
|
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
|
||||||
|
|
||||||
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table
|
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
|
||||||
elif filepath.ext == ".toml":
|
elif filepath.ext == ".toml":
|
||||||
import toml
|
import toml
|
||||||
|
|
||||||
|
@ -83,10 +83,8 @@ def locate_config(
|
||||||
) -> Tuple[
|
) -> Tuple[
|
||||||
Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]],
|
Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]],
|
||||||
]:
|
]:
|
||||||
"""
|
"""Search in the list of arguments for a valid ini-file for pytest,
|
||||||
Search in the list of arguments for a valid ini-file for pytest,
|
and return a tuple of (rootdir, inifile, cfg-dict)."""
|
||||||
and return a tuple of (rootdir, inifile, cfg-dict).
|
|
||||||
"""
|
|
||||||
config_names = [
|
config_names = [
|
||||||
"pytest.ini",
|
"pytest.ini",
|
||||||
"pyproject.toml",
|
"pyproject.toml",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" interactive debugging with PDB, the Python Debugger. """
|
"""Interactive debugging with PDB, the Python Debugger."""
|
||||||
import argparse
|
import argparse
|
||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
|
@ -87,7 +87,7 @@ def pytest_configure(config: Config) -> None:
|
||||||
|
|
||||||
|
|
||||||
class pytestPDB:
|
class pytestPDB:
|
||||||
""" Pseudo PDB that defers to the real pdb. """
|
"""Pseudo PDB that defers to the real pdb."""
|
||||||
|
|
||||||
_pluginmanager = None # type: PytestPluginManager
|
_pluginmanager = None # type: PytestPluginManager
|
||||||
_config = None # type: Config
|
_config = None # type: Config
|
||||||
|
@ -226,7 +226,7 @@ class pytestPDB:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _init_pdb(cls, method, *args, **kwargs):
|
def _init_pdb(cls, method, *args, **kwargs):
|
||||||
""" Initialize PDB debugging, dropping any IO capturing. """
|
"""Initialize PDB debugging, dropping any IO capturing."""
|
||||||
import _pytest.config
|
import _pytest.config
|
||||||
|
|
||||||
if cls._pluginmanager is not None:
|
if cls._pluginmanager is not None:
|
||||||
|
@ -298,16 +298,16 @@ class PdbTrace:
|
||||||
|
|
||||||
|
|
||||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||||
"""Changes the python function object of the given Function item by a wrapper which actually
|
"""Change the Python function object of the given Function item by a
|
||||||
enters pdb before calling the python function itself, effectively leaving the user
|
wrapper which actually enters pdb before calling the python function
|
||||||
in the pdb prompt in the first statement of the function.
|
itself, effectively leaving the user in the pdb prompt in the first
|
||||||
"""
|
statement of the function."""
|
||||||
_pdb = pytestPDB._init_pdb("runcall")
|
_pdb = pytestPDB._init_pdb("runcall")
|
||||||
testfunction = pyfuncitem.obj
|
testfunction = pyfuncitem.obj
|
||||||
|
|
||||||
# we can't just return `partial(pdb.runcall, testfunction)` because (on
|
# we can't just return `partial(pdb.runcall, testfunction)` because (on
|
||||||
# python < 3.7.4) runcall's first param is `func`, which means we'd get
|
# python < 3.7.4) runcall's first param is `func`, which means we'd get
|
||||||
# an exception if one of the kwargs to testfunction was called `func`
|
# an exception if one of the kwargs to testfunction was called `func`.
|
||||||
@functools.wraps(testfunction)
|
@functools.wraps(testfunction)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
func = functools.partial(testfunction, *args, **kwargs)
|
func = functools.partial(testfunction, *args, **kwargs)
|
||||||
|
@ -318,7 +318,7 @@ def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||||
|
|
||||||
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
|
def maybe_wrap_pytest_function_for_tracing(pyfuncitem):
|
||||||
"""Wrap the given pytestfunct item for tracing support if --trace was given in
|
"""Wrap the given pytestfunct item for tracing support if --trace was given in
|
||||||
the command line"""
|
the command line."""
|
||||||
if pyfuncitem.config.getvalue("trace"):
|
if pyfuncitem.config.getvalue("trace"):
|
||||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""
|
"""Deprecation messages and bits of code used elsewhere in the codebase that
|
||||||
This module contains deprecation messages and bits of code used elsewhere in the codebase
|
is planned to be removed in the next pytest release.
|
||||||
that is planned to be removed in the next pytest release.
|
|
||||||
|
|
||||||
Keeping it in a central location makes it easy to track what is deprecated and should
|
Keeping it in a central location makes it easy to track what is deprecated and should
|
||||||
be removed when the time comes.
|
be removed when the time comes.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" discover and run doctests in modules and test files."""
|
"""Discover and run doctests in modules and test files."""
|
||||||
import bdb
|
import bdb
|
||||||
import inspect
|
import inspect
|
||||||
import platform
|
import platform
|
||||||
|
@ -171,9 +171,10 @@ def _init_runner_class() -> "Type[doctest.DocTestRunner]":
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
class PytestDoctestRunner(doctest.DebugRunner):
|
class PytestDoctestRunner(doctest.DebugRunner):
|
||||||
"""
|
"""Runner to collect failures.
|
||||||
Runner to collect failures. Note that the out variable in this case is
|
|
||||||
a list instead of a stdout-like object
|
Note that the out variable in this case is a list instead of a
|
||||||
|
stdout-like object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -261,9 +262,7 @@ class DoctestItem(pytest.Item):
|
||||||
dtest: "doctest.DocTest"
|
dtest: "doctest.DocTest"
|
||||||
):
|
):
|
||||||
# incompatible signature due to to imposed limits on sublcass
|
# incompatible signature due to to imposed limits on sublcass
|
||||||
"""
|
"""The public named constructor."""
|
||||||
the public named constructor
|
|
||||||
"""
|
|
||||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
|
@ -289,9 +288,7 @@ class DoctestItem(pytest.Item):
|
||||||
raise MultipleDoctestFailures(failures)
|
raise MultipleDoctestFailures(failures)
|
||||||
|
|
||||||
def _disable_output_capturing_for_darwin(self) -> None:
|
def _disable_output_capturing_for_darwin(self) -> None:
|
||||||
"""
|
"""Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
|
||||||
Disable output capturing. Otherwise, stdout is lost to doctest (#985)
|
|
||||||
"""
|
|
||||||
if platform.system() != "Darwin":
|
if platform.system() != "Darwin":
|
||||||
return
|
return
|
||||||
capman = self.config.pluginmanager.getplugin("capturemanager")
|
capman = self.config.pluginmanager.getplugin("capturemanager")
|
||||||
|
@ -403,7 +400,7 @@ def _get_continue_on_failure(config):
|
||||||
continue_on_failure = config.getvalue("doctest_continue_on_failure")
|
continue_on_failure = config.getvalue("doctest_continue_on_failure")
|
||||||
if continue_on_failure:
|
if continue_on_failure:
|
||||||
# We need to turn off this if we use pdb since we should stop at
|
# We need to turn off this if we use pdb since we should stop at
|
||||||
# the first failure
|
# the first failure.
|
||||||
if config.getvalue("usepdb"):
|
if config.getvalue("usepdb"):
|
||||||
continue_on_failure = False
|
continue_on_failure = False
|
||||||
return continue_on_failure
|
return continue_on_failure
|
||||||
|
@ -415,8 +412,8 @@ class DoctestTextfile(pytest.Module):
|
||||||
def collect(self) -> Iterable[DoctestItem]:
|
def collect(self) -> Iterable[DoctestItem]:
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
# inspired by doctest.testfile; ideally we would use it directly,
|
# Inspired by doctest.testfile; ideally we would use it directly,
|
||||||
# but it doesn't support passing a custom checker
|
# but it doesn't support passing a custom checker.
|
||||||
encoding = self.config.getini("doctest_encoding")
|
encoding = self.config.getini("doctest_encoding")
|
||||||
text = self.fspath.read_text(encoding)
|
text = self.fspath.read_text(encoding)
|
||||||
filename = str(self.fspath)
|
filename = str(self.fspath)
|
||||||
|
@ -441,9 +438,8 @@ class DoctestTextfile(pytest.Module):
|
||||||
|
|
||||||
|
|
||||||
def _check_all_skipped(test: "doctest.DocTest") -> None:
|
def _check_all_skipped(test: "doctest.DocTest") -> None:
|
||||||
"""raises pytest.skip() if all examples in the given DocTest have the SKIP
|
"""Raise pytest.skip() if all examples in the given DocTest have the SKIP
|
||||||
option set.
|
option set."""
|
||||||
"""
|
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
|
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
|
||||||
|
@ -452,9 +448,8 @@ def _check_all_skipped(test: "doctest.DocTest") -> None:
|
||||||
|
|
||||||
|
|
||||||
def _is_mocked(obj: object) -> bool:
|
def _is_mocked(obj: object) -> bool:
|
||||||
"""
|
"""Return if an object is possibly a mock object by checking the
|
||||||
returns if a object is possibly a mock object by checking the existence of a highly improbable attribute
|
existence of a highly improbable attribute."""
|
||||||
"""
|
|
||||||
return (
|
return (
|
||||||
safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
|
safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
|
||||||
is not None
|
is not None
|
||||||
|
@ -463,10 +458,8 @@ def _is_mocked(obj: object) -> bool:
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
|
def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
|
||||||
"""
|
"""Context manager which replaces ``inspect.unwrap`` with a version
|
||||||
contextmanager which replaces ``inspect.unwrap`` with a version
|
that's aware of mock objects and doesn't recurse into them."""
|
||||||
that's aware of mock objects and doesn't recurse on them
|
|
||||||
"""
|
|
||||||
real_unwrap = inspect.unwrap
|
real_unwrap = inspect.unwrap
|
||||||
|
|
||||||
def _mock_aware_unwrap(
|
def _mock_aware_unwrap(
|
||||||
|
@ -498,16 +491,15 @@ class DoctestModule(pytest.Module):
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
class MockAwareDocTestFinder(doctest.DocTestFinder):
|
class MockAwareDocTestFinder(doctest.DocTestFinder):
|
||||||
"""
|
"""A hackish doctest finder that overrides stdlib internals to fix a stdlib bug.
|
||||||
a hackish doctest finder that overrides stdlib internals to fix a stdlib bug
|
|
||||||
|
|
||||||
https://github.com/pytest-dev/pytest/issues/3456
|
https://github.com/pytest-dev/pytest/issues/3456
|
||||||
https://bugs.python.org/issue25532
|
https://bugs.python.org/issue25532
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _find_lineno(self, obj, source_lines):
|
def _find_lineno(self, obj, source_lines):
|
||||||
"""
|
"""Doctest code does not take into account `@property`, this
|
||||||
Doctest code does not take into account `@property`, this is a hackish way to fix it.
|
is a hackish way to fix it.
|
||||||
|
|
||||||
https://bugs.python.org/issue17446
|
https://bugs.python.org/issue17446
|
||||||
"""
|
"""
|
||||||
|
@ -542,7 +534,7 @@ class DoctestModule(pytest.Module):
|
||||||
pytest.skip("unable to import module %r" % self.fspath)
|
pytest.skip("unable to import module %r" % self.fspath)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
# uses internal doctest module parsing mechanism
|
# Uses internal doctest module parsing mechanism.
|
||||||
finder = MockAwareDocTestFinder()
|
finder = MockAwareDocTestFinder()
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self)
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
|
@ -560,9 +552,7 @@ class DoctestModule(pytest.Module):
|
||||||
|
|
||||||
|
|
||||||
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
|
||||||
"""
|
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
|
||||||
Used by DoctestTextfile and DoctestItem to setup fixture information.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def func() -> None:
|
def func() -> None:
|
||||||
pass
|
pass
|
||||||
|
@ -582,11 +572,9 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]":
|
||||||
import re
|
import re
|
||||||
|
|
||||||
class LiteralsOutputChecker(doctest.OutputChecker):
|
class LiteralsOutputChecker(doctest.OutputChecker):
|
||||||
"""
|
# Based on doctest_nose_plugin.py from the nltk project
|
||||||
Based on doctest_nose_plugin.py from the nltk project
|
# (https://github.com/nltk/nltk) and on the "numtest" doctest extension
|
||||||
(https://github.com/nltk/nltk) and on the "numtest" doctest extension
|
# by Sebastien Boisgerault (https://github.com/boisgera/numtest).
|
||||||
by Sebastien Boisgerault (https://github.com/boisgera/numtest).
|
|
||||||
"""
|
|
||||||
|
|
||||||
_unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
|
_unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
|
||||||
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
|
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
|
||||||
|
@ -671,8 +659,7 @@ def _init_checker_class() -> "Type[doctest.OutputChecker]":
|
||||||
|
|
||||||
|
|
||||||
def _get_checker() -> "doctest.OutputChecker":
|
def _get_checker() -> "doctest.OutputChecker":
|
||||||
"""
|
"""Return a doctest.OutputChecker subclass that supports some
|
||||||
Returns a doctest.OutputChecker subclass that supports some
|
|
||||||
additional options:
|
additional options:
|
||||||
|
|
||||||
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
|
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
|
||||||
|
@ -692,36 +679,31 @@ def _get_checker() -> "doctest.OutputChecker":
|
||||||
|
|
||||||
|
|
||||||
def _get_allow_unicode_flag() -> int:
|
def _get_allow_unicode_flag() -> int:
|
||||||
"""
|
"""Register and return the ALLOW_UNICODE flag."""
|
||||||
Registers and returns the ALLOW_UNICODE flag.
|
|
||||||
"""
|
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
return doctest.register_optionflag("ALLOW_UNICODE")
|
return doctest.register_optionflag("ALLOW_UNICODE")
|
||||||
|
|
||||||
|
|
||||||
def _get_allow_bytes_flag() -> int:
|
def _get_allow_bytes_flag() -> int:
|
||||||
"""
|
"""Register and return the ALLOW_BYTES flag."""
|
||||||
Registers and returns the ALLOW_BYTES flag.
|
|
||||||
"""
|
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
return doctest.register_optionflag("ALLOW_BYTES")
|
return doctest.register_optionflag("ALLOW_BYTES")
|
||||||
|
|
||||||
|
|
||||||
def _get_number_flag() -> int:
|
def _get_number_flag() -> int:
|
||||||
"""
|
"""Register and return the NUMBER flag."""
|
||||||
Registers and returns the NUMBER flag.
|
|
||||||
"""
|
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
return doctest.register_optionflag("NUMBER")
|
return doctest.register_optionflag("NUMBER")
|
||||||
|
|
||||||
|
|
||||||
def _get_report_choice(key: str) -> int:
|
def _get_report_choice(key: str) -> int:
|
||||||
"""
|
"""Return the actual `doctest` module flag value.
|
||||||
This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
|
|
||||||
importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
|
We want to do it as late as possible to avoid importing `doctest` and all
|
||||||
|
its dependencies when parsing options, as it adds overhead and breaks tests.
|
||||||
"""
|
"""
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
|
@ -736,7 +718,6 @@ def _get_report_choice(key: str) -> int:
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def doctest_namespace() -> Dict[str, Any]:
|
def doctest_namespace() -> Dict[str, Any]:
|
||||||
"""
|
"""Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.
|
namespace of doctests."""
|
||||||
"""
|
|
||||||
return dict()
|
return dict()
|
||||||
|
|
|
@ -100,8 +100,7 @@ class FaultHandlerHooks:
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True)
|
@pytest.hookimpl(tryfirst=True)
|
||||||
def pytest_enter_pdb(self) -> None:
|
def pytest_enter_pdb(self) -> None:
|
||||||
"""Cancel any traceback dumping due to timeout before entering pdb.
|
"""Cancel any traceback dumping due to timeout before entering pdb."""
|
||||||
"""
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
faulthandler.cancel_dump_traceback_later()
|
faulthandler.cancel_dump_traceback_later()
|
||||||
|
@ -109,8 +108,7 @@ class FaultHandlerHooks:
|
||||||
@pytest.hookimpl(tryfirst=True)
|
@pytest.hookimpl(tryfirst=True)
|
||||||
def pytest_exception_interact(self) -> None:
|
def pytest_exception_interact(self) -> None:
|
||||||
"""Cancel any traceback dumping due to an interactive exception being
|
"""Cancel any traceback dumping due to an interactive exception being
|
||||||
raised.
|
raised."""
|
||||||
"""
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
faulthandler.cancel_dump_traceback_later()
|
faulthandler.cancel_dump_traceback_later()
|
||||||
|
|
|
@ -166,15 +166,16 @@ def get_scope_node(node, scope):
|
||||||
def add_funcarg_pseudo_fixture_def(
|
def add_funcarg_pseudo_fixture_def(
|
||||||
collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
|
collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
|
||||||
) -> None:
|
) -> None:
|
||||||
# this function will transform all collected calls to a functions
|
# This function will transform all collected calls to functions
|
||||||
# if they use direct funcargs (i.e. direct parametrization)
|
# if they use direct funcargs (i.e. direct parametrization)
|
||||||
# because we want later test execution to be able to rely on
|
# because we want later test execution to be able to rely on
|
||||||
# an existing FixtureDef structure for all arguments.
|
# an existing FixtureDef structure for all arguments.
|
||||||
# XXX we can probably avoid this algorithm if we modify CallSpec2
|
# XXX we can probably avoid this algorithm if we modify CallSpec2
|
||||||
# to directly care for creating the fixturedefs within its methods.
|
# to directly care for creating the fixturedefs within its methods.
|
||||||
if not metafunc._calls[0].funcargs:
|
if not metafunc._calls[0].funcargs:
|
||||||
return # this function call does not have direct parametrization
|
# This function call does not have direct parametrization.
|
||||||
# collect funcargs of all callspecs into a list of values
|
return
|
||||||
|
# Collect funcargs of all callspecs into a list of values.
|
||||||
arg2params = {} # type: Dict[str, List[object]]
|
arg2params = {} # type: Dict[str, List[object]]
|
||||||
arg2scope = {} # type: Dict[str, _Scope]
|
arg2scope = {} # type: Dict[str, _Scope]
|
||||||
for callspec in metafunc._calls:
|
for callspec in metafunc._calls:
|
||||||
|
@ -189,11 +190,11 @@ def add_funcarg_pseudo_fixture_def(
|
||||||
arg2scope[argname] = scopes[scopenum]
|
arg2scope[argname] = scopes[scopenum]
|
||||||
callspec.funcargs.clear()
|
callspec.funcargs.clear()
|
||||||
|
|
||||||
# register artificial FixtureDef's so that later at test execution
|
# Register artificial FixtureDef's so that later at test execution
|
||||||
# time we can rely on a proper FixtureDef to exist for fixture setup.
|
# time we can rely on a proper FixtureDef to exist for fixture setup.
|
||||||
arg2fixturedefs = metafunc._arg2fixturedefs
|
arg2fixturedefs = metafunc._arg2fixturedefs
|
||||||
for argname, valuelist in arg2params.items():
|
for argname, valuelist in arg2params.items():
|
||||||
# if we have a scope that is higher than function we need
|
# If we have a scope that is higher than function, we need
|
||||||
# to make sure we only ever create an according fixturedef on
|
# to make sure we only ever create an according fixturedef on
|
||||||
# a per-scope basis. We thus store and cache the fixturedef on the
|
# a per-scope basis. We thus store and cache the fixturedef on the
|
||||||
# node related to the scope.
|
# node related to the scope.
|
||||||
|
@ -203,7 +204,7 @@ def add_funcarg_pseudo_fixture_def(
|
||||||
node = get_scope_node(collector, scope)
|
node = get_scope_node(collector, scope)
|
||||||
if node is None:
|
if node is None:
|
||||||
assert scope == "class" and isinstance(collector, _pytest.python.Module)
|
assert scope == "class" and isinstance(collector, _pytest.python.Module)
|
||||||
# use module-level collector for class-scope (for now)
|
# Use module-level collector for class-scope (for now).
|
||||||
node = collector
|
node = collector
|
||||||
if node and argname in node._name2pseudofixturedef:
|
if node and argname in node._name2pseudofixturedef:
|
||||||
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
|
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
|
||||||
|
@ -224,7 +225,7 @@ def add_funcarg_pseudo_fixture_def(
|
||||||
|
|
||||||
|
|
||||||
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
|
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
|
||||||
""" return fixturemarker or None if it doesn't exist or raised
|
"""Return fixturemarker or None if it doesn't exist or raised
|
||||||
exceptions."""
|
exceptions."""
|
||||||
try:
|
try:
|
||||||
fixturemarker = getattr(
|
fixturemarker = getattr(
|
||||||
|
@ -242,7 +243,7 @@ _Key = Tuple[object, ...]
|
||||||
|
|
||||||
|
|
||||||
def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]:
|
def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator[_Key]:
|
||||||
""" return list of keys for all parametrized arguments which match
|
"""Return list of keys for all parametrized arguments which match
|
||||||
the specified scope. """
|
the specified scope. """
|
||||||
assert scopenum < scopenum_function # function
|
assert scopenum < scopenum_function # function
|
||||||
try:
|
try:
|
||||||
|
@ -269,10 +270,10 @@ def get_parametrized_fixture_keys(item: "nodes.Item", scopenum: int) -> Iterator
|
||||||
yield key
|
yield key
|
||||||
|
|
||||||
|
|
||||||
# algorithm for sorting on a per-parametrized resource setup basis
|
# Algorithm for sorting on a per-parametrized resource setup basis.
|
||||||
# it is called for scopenum==0 (session) first and performs sorting
|
# It is called for scopenum==0 (session) first and performs sorting
|
||||||
# down to the lower scopes such as to minimize number of "high scope"
|
# down to the lower scopes such as to minimize number of "high scope"
|
||||||
# setups and teardowns
|
# setups and teardowns.
|
||||||
|
|
||||||
|
|
||||||
def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]":
|
def reorder_items(items: "Sequence[nodes.Item]") -> "List[nodes.Item]":
|
||||||
|
@ -339,7 +340,8 @@ def reorder_items_atscope(
|
||||||
no_argkey_group[item] = None
|
no_argkey_group[item] = None
|
||||||
else:
|
else:
|
||||||
slicing_argkey, _ = argkeys.popitem()
|
slicing_argkey, _ = argkeys.popitem()
|
||||||
# we don't have to remove relevant items from later in the deque because they'll just be ignored
|
# We don't have to remove relevant items from later in the
|
||||||
|
# deque because they'll just be ignored.
|
||||||
matching_items = [
|
matching_items = [
|
||||||
i for i in scoped_items_by_argkey[slicing_argkey] if i in items
|
i for i in scoped_items_by_argkey[slicing_argkey] if i in items
|
||||||
]
|
]
|
||||||
|
@ -358,7 +360,7 @@ def reorder_items_atscope(
|
||||||
|
|
||||||
|
|
||||||
def fillfixtures(function: "Function") -> None:
|
def fillfixtures(function: "Function") -> None:
|
||||||
""" fill missing funcargs for a test function. """
|
"""Fill missing funcargs for a test function."""
|
||||||
# Uncomment this after 6.0 release (#7361)
|
# Uncomment this after 6.0 release (#7361)
|
||||||
# warnings.warn(FILLFUNCARGS, stacklevel=2)
|
# warnings.warn(FILLFUNCARGS, stacklevel=2)
|
||||||
try:
|
try:
|
||||||
|
@ -373,7 +375,7 @@ def fillfixtures(function: "Function") -> None:
|
||||||
function._fixtureinfo = fi
|
function._fixtureinfo = fi
|
||||||
request = function._request = FixtureRequest(function)
|
request = function._request = FixtureRequest(function)
|
||||||
request._fillfixtures()
|
request._fillfixtures()
|
||||||
# prune out funcargs for jstests
|
# Prune out funcargs for jstests.
|
||||||
newfuncargs = {}
|
newfuncargs = {}
|
||||||
for name in fi.argnames:
|
for name in fi.argnames:
|
||||||
newfuncargs[name] = function.funcargs[name]
|
newfuncargs[name] = function.funcargs[name]
|
||||||
|
@ -388,9 +390,9 @@ def get_direct_param_fixture_func(request):
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
class FuncFixtureInfo:
|
class FuncFixtureInfo:
|
||||||
# original function argument names
|
# Original function argument names.
|
||||||
argnames = attr.ib(type=Tuple[str, ...])
|
argnames = attr.ib(type=Tuple[str, ...])
|
||||||
# argnames that function immediately requires. These include argnames +
|
# Argnames that function immediately requires. These include argnames +
|
||||||
# fixture names specified via usefixtures and via autouse=True in fixture
|
# fixture names specified via usefixtures and via autouse=True in fixture
|
||||||
# definitions.
|
# definitions.
|
||||||
initialnames = attr.ib(type=Tuple[str, ...])
|
initialnames = attr.ib(type=Tuple[str, ...])
|
||||||
|
@ -398,7 +400,7 @@ class FuncFixtureInfo:
|
||||||
name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]])
|
name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef"]])
|
||||||
|
|
||||||
def prune_dependency_tree(self) -> None:
|
def prune_dependency_tree(self) -> None:
|
||||||
"""Recompute names_closure from initialnames and name2fixturedefs
|
"""Recompute names_closure from initialnames and name2fixturedefs.
|
||||||
|
|
||||||
Can only reduce names_closure, which means that the new closure will
|
Can only reduce names_closure, which means that the new closure will
|
||||||
always be a subset of the old one. The order is preserved.
|
always be a subset of the old one. The order is preserved.
|
||||||
|
@ -412,7 +414,7 @@ class FuncFixtureInfo:
|
||||||
working_set = set(self.initialnames)
|
working_set = set(self.initialnames)
|
||||||
while working_set:
|
while working_set:
|
||||||
argname = working_set.pop()
|
argname = working_set.pop()
|
||||||
# argname may be smth not included in the original names_closure,
|
# Argname may be smth not included in the original names_closure,
|
||||||
# in which case we ignore it. This currently happens with pseudo
|
# in which case we ignore it. This currently happens with pseudo
|
||||||
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
|
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
|
||||||
# So they introduce the new dependency 'request' which might have
|
# So they introduce the new dependency 'request' which might have
|
||||||
|
@ -426,18 +428,18 @@ class FuncFixtureInfo:
|
||||||
|
|
||||||
|
|
||||||
class FixtureRequest:
|
class FixtureRequest:
|
||||||
""" A request for a fixture from a test or fixture function.
|
"""A request for a fixture from a test or fixture function.
|
||||||
|
|
||||||
A request object gives access to the requesting test context
|
A request object gives access to the requesting test context and has
|
||||||
and has an optional ``param`` attribute in case
|
an optional ``param`` attribute in case the fixture is parametrized
|
||||||
the fixture is parametrized indirectly.
|
indirectly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pyfuncitem) -> None:
|
def __init__(self, pyfuncitem) -> None:
|
||||||
self._pyfuncitem = pyfuncitem
|
self._pyfuncitem = pyfuncitem
|
||||||
#: fixture for which this request is being performed
|
#: Fixture for which this request is being performed.
|
||||||
self.fixturename = None # type: Optional[str]
|
self.fixturename = None # type: Optional[str]
|
||||||
#: Scope string, one of "function", "class", "module", "session"
|
#: Scope string, one of "function", "class", "module", "session".
|
||||||
self.scope = "function" # type: _Scope
|
self.scope = "function" # type: _Scope
|
||||||
self._fixture_defs = {} # type: Dict[str, FixtureDef]
|
self._fixture_defs = {} # type: Dict[str, FixtureDef]
|
||||||
fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo
|
fixtureinfo = pyfuncitem._fixtureinfo # type: FuncFixtureInfo
|
||||||
|
@ -449,35 +451,35 @@ class FixtureRequest:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fixturenames(self) -> List[str]:
|
def fixturenames(self) -> List[str]:
|
||||||
"""names of all active fixtures in this request"""
|
"""Names of all active fixtures in this request."""
|
||||||
result = list(self._pyfuncitem._fixtureinfo.names_closure)
|
result = list(self._pyfuncitem._fixtureinfo.names_closure)
|
||||||
result.extend(set(self._fixture_defs).difference(result))
|
result.extend(set(self._fixture_defs).difference(result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def funcargnames(self) -> List[str]:
|
def funcargnames(self) -> List[str]:
|
||||||
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
"""Alias attribute for ``fixturenames`` for pre-2.3 compatibility."""
|
||||||
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
||||||
return self.fixturenames
|
return self.fixturenames
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def node(self):
|
def node(self):
|
||||||
""" underlying collection node (depends on current request scope)"""
|
"""Underlying collection node (depends on current request scope)."""
|
||||||
return self._getscopeitem(self.scope)
|
return self._getscopeitem(self.scope)
|
||||||
|
|
||||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef":
|
def _getnextfixturedef(self, argname: str) -> "FixtureDef":
|
||||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||||
if fixturedefs is None:
|
if fixturedefs is None:
|
||||||
# we arrive here because of a dynamic call to
|
# We arrive here because of a dynamic call to
|
||||||
# getfixturevalue(argname) usage which was naturally
|
# getfixturevalue(argname) usage which was naturally
|
||||||
# not known at parsing/collection time
|
# not known at parsing/collection time.
|
||||||
assert self._pyfuncitem.parent is not None
|
assert self._pyfuncitem.parent is not None
|
||||||
parentid = self._pyfuncitem.parent.nodeid
|
parentid = self._pyfuncitem.parent.nodeid
|
||||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
|
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
|
||||||
# TODO: Fix this type ignore. Either add assert or adjust types.
|
# TODO: Fix this type ignore. Either add assert or adjust types.
|
||||||
# Can this be None here?
|
# Can this be None here?
|
||||||
self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment]
|
self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment]
|
||||||
# fixturedefs list is immutable so we maintain a decreasing index
|
# fixturedefs list is immutable so we maintain a decreasing index.
|
||||||
index = self._arg2index.get(argname, 0) - 1
|
index = self._arg2index.get(argname, 0) - 1
|
||||||
if fixturedefs is None or (-index > len(fixturedefs)):
|
if fixturedefs is None or (-index > len(fixturedefs)):
|
||||||
raise FixtureLookupError(argname, self)
|
raise FixtureLookupError(argname, self)
|
||||||
|
@ -486,25 +488,25 @@ class FixtureRequest:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> Config:
|
def config(self) -> Config:
|
||||||
""" the pytest config object associated with this request. """
|
"""The pytest config object associated with this request."""
|
||||||
return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723
|
return self._pyfuncitem.config # type: ignore[no-any-return] # noqa: F723
|
||||||
|
|
||||||
@scopeproperty()
|
@scopeproperty()
|
||||||
def function(self):
|
def function(self):
|
||||||
""" test function object if the request has a per-function scope. """
|
"""Test function object if the request has a per-function scope."""
|
||||||
return self._pyfuncitem.obj
|
return self._pyfuncitem.obj
|
||||||
|
|
||||||
@scopeproperty("class")
|
@scopeproperty("class")
|
||||||
def cls(self):
|
def cls(self):
|
||||||
""" class (can be None) where the test function was collected. """
|
"""Class (can be None) where the test function was collected."""
|
||||||
clscol = self._pyfuncitem.getparent(_pytest.python.Class)
|
clscol = self._pyfuncitem.getparent(_pytest.python.Class)
|
||||||
if clscol:
|
if clscol:
|
||||||
return clscol.obj
|
return clscol.obj
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def instance(self):
|
def instance(self):
|
||||||
""" instance (can be None) on which test function was collected. """
|
"""Instance (can be None) on which test function was collected."""
|
||||||
# unittest support hack, see _pytest.unittest.TestCaseFunction
|
# unittest support hack, see _pytest.unittest.TestCaseFunction.
|
||||||
try:
|
try:
|
||||||
return self._pyfuncitem._testcase
|
return self._pyfuncitem._testcase
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -513,30 +515,29 @@ class FixtureRequest:
|
||||||
|
|
||||||
@scopeproperty()
|
@scopeproperty()
|
||||||
def module(self):
|
def module(self):
|
||||||
""" python module object where the test function was collected. """
|
"""Python module object where the test function was collected."""
|
||||||
return self._pyfuncitem.getparent(_pytest.python.Module).obj
|
return self._pyfuncitem.getparent(_pytest.python.Module).obj
|
||||||
|
|
||||||
@scopeproperty()
|
@scopeproperty()
|
||||||
def fspath(self) -> py.path.local:
|
def fspath(self) -> py.path.local:
|
||||||
""" the file system path of the test module which collected this test. """
|
"""The file system path of the test module which collected this test."""
|
||||||
# TODO: Remove ignore once _pyfuncitem is properly typed.
|
# TODO: Remove ignore once _pyfuncitem is properly typed.
|
||||||
return self._pyfuncitem.fspath # type: ignore
|
return self._pyfuncitem.fspath # type: ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keywords(self):
|
def keywords(self):
|
||||||
""" keywords/markers dictionary for the underlying node. """
|
"""Keywords/markers dictionary for the underlying node."""
|
||||||
return self.node.keywords
|
return self.node.keywords
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session(self):
|
def session(self):
|
||||||
""" pytest session object. """
|
"""Pytest session object."""
|
||||||
return self._pyfuncitem.session
|
return self._pyfuncitem.session
|
||||||
|
|
||||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||||
""" add finalizer/teardown function to be called after the
|
"""Add finalizer/teardown function to be called after the last test
|
||||||
last test within the requesting test context finished
|
within the requesting test context finished execution."""
|
||||||
execution. """
|
# XXX usually this method is shadowed by fixturedef specific ones.
|
||||||
# XXX usually this method is shadowed by fixturedef specific ones
|
|
||||||
self._addfinalizer(finalizer, scope=self.scope)
|
self._addfinalizer(finalizer, scope=self.scope)
|
||||||
|
|
||||||
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
||||||
|
@ -546,17 +547,19 @@ class FixtureRequest:
|
||||||
)
|
)
|
||||||
|
|
||||||
def applymarker(self, marker) -> None:
|
def applymarker(self, marker) -> None:
|
||||||
""" Apply a marker to a single test function invocation.
|
"""Apply a marker to a single test function invocation.
|
||||||
|
|
||||||
This method is useful if you don't want to have a keyword/marker
|
This method is useful if you don't want to have a keyword/marker
|
||||||
on all function invocations.
|
on all function invocations.
|
||||||
|
|
||||||
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
|
:param marker:
|
||||||
created by a call to ``pytest.mark.NAME(...)``.
|
A :py:class:`_pytest.mark.MarkDecorator` object created by a call
|
||||||
|
to ``pytest.mark.NAME(...)``.
|
||||||
"""
|
"""
|
||||||
self.node.add_marker(marker)
|
self.node.add_marker(marker)
|
||||||
|
|
||||||
def raiseerror(self, msg: Optional[str]) -> "NoReturn":
|
def raiseerror(self, msg: Optional[str]) -> "NoReturn":
|
||||||
""" raise a FixtureLookupError with the given message. """
|
"""Raise a FixtureLookupError with the given message."""
|
||||||
raise self._fixturemanager.FixtureLookupError(None, self, msg)
|
raise self._fixturemanager.FixtureLookupError(None, self, msg)
|
||||||
|
|
||||||
def _fillfixtures(self) -> None:
|
def _fillfixtures(self) -> None:
|
||||||
|
@ -567,14 +570,14 @@ class FixtureRequest:
|
||||||
item.funcargs[argname] = self.getfixturevalue(argname)
|
item.funcargs[argname] = self.getfixturevalue(argname)
|
||||||
|
|
||||||
def getfixturevalue(self, argname: str) -> Any:
|
def getfixturevalue(self, argname: str) -> Any:
|
||||||
""" Dynamically run a named fixture function.
|
"""Dynamically run a named fixture function.
|
||||||
|
|
||||||
Declaring fixtures via function argument is recommended where possible.
|
Declaring fixtures via function argument is recommended where possible.
|
||||||
But if you can only decide whether to use another fixture at test
|
But if you can only decide whether to use another fixture at test
|
||||||
setup time, you may use this function to retrieve it inside a fixture
|
setup time, you may use this function to retrieve it inside a fixture
|
||||||
or test function body.
|
or test function body.
|
||||||
|
|
||||||
:raise pytest.FixtureLookupError:
|
:raises pytest.FixtureLookupError:
|
||||||
If the given fixture could not be found.
|
If the given fixture could not be found.
|
||||||
"""
|
"""
|
||||||
fixturedef = self._get_active_fixturedef(argname)
|
fixturedef = self._get_active_fixturedef(argname)
|
||||||
|
@ -595,8 +598,8 @@ class FixtureRequest:
|
||||||
scope = "function" # type: _Scope
|
scope = "function" # type: _Scope
|
||||||
return PseudoFixtureDef(cached_result, scope)
|
return PseudoFixtureDef(cached_result, scope)
|
||||||
raise
|
raise
|
||||||
# remove indent to prevent the python3 exception
|
# Remove indent to prevent the python3 exception
|
||||||
# from leaking into the call
|
# from leaking into the call.
|
||||||
self._compute_fixture_value(fixturedef)
|
self._compute_fixture_value(fixturedef)
|
||||||
self._fixture_defs[argname] = fixturedef
|
self._fixture_defs[argname] = fixturedef
|
||||||
return fixturedef
|
return fixturedef
|
||||||
|
@ -614,10 +617,12 @@ class FixtureRequest:
|
||||||
current = current._parent_request
|
current = current._parent_request
|
||||||
|
|
||||||
def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None:
|
def _compute_fixture_value(self, fixturedef: "FixtureDef") -> None:
|
||||||
"""
|
"""Create a SubRequest based on "self" and call the execute method
|
||||||
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
|
of the given FixtureDef object.
|
||||||
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
|
|
||||||
will be stored into the FixtureDef object itself.
|
This will force the FixtureDef object to throw away any previous
|
||||||
|
results and compute a new fixture value, which will be stored into
|
||||||
|
the FixtureDef object itself.
|
||||||
"""
|
"""
|
||||||
# prepare a subrequest object before calling fixture function
|
# prepare a subrequest object before calling fixture function
|
||||||
# (latter managed by fixturedef)
|
# (latter managed by fixturedef)
|
||||||
|
@ -667,18 +672,18 @@ class FixtureRequest:
|
||||||
fail(msg, pytrace=False)
|
fail(msg, pytrace=False)
|
||||||
else:
|
else:
|
||||||
param_index = funcitem.callspec.indices[argname]
|
param_index = funcitem.callspec.indices[argname]
|
||||||
# if a parametrize invocation set a scope it will override
|
# If a parametrize invocation set a scope it will override
|
||||||
# the static scope defined with the fixture function
|
# the static scope defined with the fixture function.
|
||||||
paramscopenum = funcitem.callspec._arg2scopenum.get(argname)
|
paramscopenum = funcitem.callspec._arg2scopenum.get(argname)
|
||||||
if paramscopenum is not None:
|
if paramscopenum is not None:
|
||||||
scope = scopes[paramscopenum]
|
scope = scopes[paramscopenum]
|
||||||
|
|
||||||
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
|
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
|
||||||
|
|
||||||
# check if a higher-level scoped fixture accesses a lower level one
|
# Check if a higher-level scoped fixture accesses a lower level one.
|
||||||
subrequest._check_scope(argname, self.scope, scope)
|
subrequest._check_scope(argname, self.scope, scope)
|
||||||
try:
|
try:
|
||||||
# call the fixture function
|
# Call the fixture function.
|
||||||
fixturedef.execute(request=subrequest)
|
fixturedef.execute(request=subrequest)
|
||||||
finally:
|
finally:
|
||||||
self._schedule_finalizers(fixturedef, subrequest)
|
self._schedule_finalizers(fixturedef, subrequest)
|
||||||
|
@ -686,7 +691,7 @@ class FixtureRequest:
|
||||||
def _schedule_finalizers(
|
def _schedule_finalizers(
|
||||||
self, fixturedef: "FixtureDef", subrequest: "SubRequest"
|
self, fixturedef: "FixtureDef", subrequest: "SubRequest"
|
||||||
) -> None:
|
) -> None:
|
||||||
# if fixture function failed it might have registered finalizers
|
# If fixture function failed it might have registered finalizers.
|
||||||
self.session._setupstate.addfinalizer(
|
self.session._setupstate.addfinalizer(
|
||||||
functools.partial(fixturedef.finish, request=subrequest), subrequest.node
|
functools.partial(fixturedef.finish, request=subrequest), subrequest.node
|
||||||
)
|
)
|
||||||
|
@ -695,7 +700,7 @@ class FixtureRequest:
|
||||||
if argname == "request":
|
if argname == "request":
|
||||||
return
|
return
|
||||||
if scopemismatch(invoking_scope, requested_scope):
|
if scopemismatch(invoking_scope, requested_scope):
|
||||||
# try to report something helpful
|
# Try to report something helpful.
|
||||||
lines = self._factorytraceback()
|
lines = self._factorytraceback()
|
||||||
fail(
|
fail(
|
||||||
"ScopeMismatch: You tried to access the %r scoped "
|
"ScopeMismatch: You tried to access the %r scoped "
|
||||||
|
@ -717,7 +722,7 @@ class FixtureRequest:
|
||||||
|
|
||||||
def _getscopeitem(self, scope):
|
def _getscopeitem(self, scope):
|
||||||
if scope == "function":
|
if scope == "function":
|
||||||
# this might also be a non-function Item despite its attribute name
|
# This might also be a non-function Item despite its attribute name.
|
||||||
return self._pyfuncitem
|
return self._pyfuncitem
|
||||||
if scope == "package":
|
if scope == "package":
|
||||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||||
|
@ -726,7 +731,7 @@ class FixtureRequest:
|
||||||
else:
|
else:
|
||||||
node = get_scope_node(self._pyfuncitem, scope)
|
node = get_scope_node(self._pyfuncitem, scope)
|
||||||
if node is None and scope == "class":
|
if node is None and scope == "class":
|
||||||
# fallback to function item itself
|
# Fallback to function item itself.
|
||||||
node = self._pyfuncitem
|
node = self._pyfuncitem
|
||||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||||
scope, self._pyfuncitem
|
scope, self._pyfuncitem
|
||||||
|
@ -738,8 +743,7 @@ class FixtureRequest:
|
||||||
|
|
||||||
|
|
||||||
class SubRequest(FixtureRequest):
|
class SubRequest(FixtureRequest):
|
||||||
""" a sub request for handling getting a fixture from a
|
"""A sub request for handling getting a fixture from a test function/fixture."""
|
||||||
test function/fixture. """
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -750,7 +754,7 @@ class SubRequest(FixtureRequest):
|
||||||
fixturedef: "FixtureDef",
|
fixturedef: "FixtureDef",
|
||||||
) -> None:
|
) -> None:
|
||||||
self._parent_request = request
|
self._parent_request = request
|
||||||
self.fixturename = fixturedef.argname # type: str
|
self.fixturename = fixturedef.argname
|
||||||
if param is not NOTSET:
|
if param is not NOTSET:
|
||||||
self.param = param
|
self.param = param
|
||||||
self.param_index = param_index
|
self.param_index = param_index
|
||||||
|
@ -771,9 +775,9 @@ class SubRequest(FixtureRequest):
|
||||||
def _schedule_finalizers(
|
def _schedule_finalizers(
|
||||||
self, fixturedef: "FixtureDef", subrequest: "SubRequest"
|
self, fixturedef: "FixtureDef", subrequest: "SubRequest"
|
||||||
) -> None:
|
) -> None:
|
||||||
# if the executing fixturedef was not explicitly requested in the argument list (via
|
# If the executing fixturedef was not explicitly requested in the argument list (via
|
||||||
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
||||||
# first
|
# first.
|
||||||
if fixturedef.argname not in self.fixturenames:
|
if fixturedef.argname not in self.fixturenames:
|
||||||
fixturedef.addfinalizer(
|
fixturedef.addfinalizer(
|
||||||
functools.partial(self._fixturedef.finish, request=self)
|
functools.partial(self._fixturedef.finish, request=self)
|
||||||
|
@ -791,8 +795,7 @@ def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool:
|
||||||
|
|
||||||
def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
|
def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
|
||||||
"""Look up the index of ``scope`` and raise a descriptive value error
|
"""Look up the index of ``scope`` and raise a descriptive value error
|
||||||
if not defined.
|
if not defined."""
|
||||||
"""
|
|
||||||
strscopes = scopes # type: Sequence[str]
|
strscopes = scopes # type: Sequence[str]
|
||||||
try:
|
try:
|
||||||
return strscopes.index(scope)
|
return strscopes.index(scope)
|
||||||
|
@ -806,7 +809,7 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
|
||||||
|
|
||||||
|
|
||||||
class FixtureLookupError(LookupError):
|
class FixtureLookupError(LookupError):
|
||||||
""" could not return a requested Fixture (missing or invalid). """
|
"""Could not return a requested fixture (missing or invalid)."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None
|
self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None
|
||||||
|
@ -823,8 +826,8 @@ class FixtureLookupError(LookupError):
|
||||||
stack.extend(map(lambda x: x.func, self.fixturestack))
|
stack.extend(map(lambda x: x.func, self.fixturestack))
|
||||||
msg = self.msg
|
msg = self.msg
|
||||||
if msg is not None:
|
if msg is not None:
|
||||||
# the last fixture raise an error, let's present
|
# The last fixture raise an error, let's present
|
||||||
# it at the requesting side
|
# it at the requesting side.
|
||||||
stack = stack[:-1]
|
stack = stack[:-1]
|
||||||
for function in stack:
|
for function in stack:
|
||||||
fspath, lineno = getfslineno(function)
|
fspath, lineno = getfslineno(function)
|
||||||
|
@ -925,8 +928,9 @@ def call_fixture_func(
|
||||||
|
|
||||||
|
|
||||||
def _teardown_yield_fixture(fixturefunc, it) -> None:
|
def _teardown_yield_fixture(fixturefunc, it) -> None:
|
||||||
"""Executes the teardown of a fixture function by advancing the iterator after the
|
"""Execute the teardown of a fixture function by advancing the iterator
|
||||||
yield and ensure the iteration ends (if not it means there is more than one yield in the function)"""
|
after the yield and ensure the iteration ends (if not it means there is
|
||||||
|
more than one yield in the function)."""
|
||||||
try:
|
try:
|
||||||
next(it)
|
next(it)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
|
@ -961,7 +965,7 @@ def _eval_scope_callable(
|
||||||
|
|
||||||
|
|
||||||
class FixtureDef(Generic[_FixtureValue]):
|
class FixtureDef(Generic[_FixtureValue]):
|
||||||
""" A container for a factory definition. """
|
"""A container for a factory definition."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -1023,16 +1027,15 @@ class FixtureDef(Generic[_FixtureValue]):
|
||||||
finally:
|
finally:
|
||||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||||
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||||
# even if finalization fails, we invalidate
|
# Even if finalization fails, we invalidate the cached fixture
|
||||||
# the cached fixture value and remove
|
# value and remove all finalizers because they may be bound methods
|
||||||
# all finalizers because they may be bound methods which will
|
# which will keep instances alive.
|
||||||
# keep instances alive
|
|
||||||
self.cached_result = None
|
self.cached_result = None
|
||||||
self._finalizers = []
|
self._finalizers = []
|
||||||
|
|
||||||
def execute(self, request: SubRequest) -> _FixtureValue:
|
def execute(self, request: SubRequest) -> _FixtureValue:
|
||||||
# get required arguments and register our own finish()
|
# Get required arguments and register our own finish()
|
||||||
# with their finalization
|
# with their finalization.
|
||||||
for argname in self.argnames:
|
for argname in self.argnames:
|
||||||
fixturedef = request._get_active_fixturedef(argname)
|
fixturedef = request._get_active_fixturedef(argname)
|
||||||
if argname != "request":
|
if argname != "request":
|
||||||
|
@ -1043,7 +1046,7 @@ class FixtureDef(Generic[_FixtureValue]):
|
||||||
my_cache_key = self.cache_key(request)
|
my_cache_key = self.cache_key(request)
|
||||||
if self.cached_result is not None:
|
if self.cached_result is not None:
|
||||||
# note: comparison with `==` can fail (or be expensive) for e.g.
|
# note: comparison with `==` can fail (or be expensive) for e.g.
|
||||||
# numpy arrays (#6497)
|
# numpy arrays (#6497).
|
||||||
cache_key = self.cached_result[1]
|
cache_key = self.cached_result[1]
|
||||||
if my_cache_key is cache_key:
|
if my_cache_key is cache_key:
|
||||||
if self.cached_result[2] is not None:
|
if self.cached_result[2] is not None:
|
||||||
|
@ -1052,8 +1055,8 @@ class FixtureDef(Generic[_FixtureValue]):
|
||||||
else:
|
else:
|
||||||
result = self.cached_result[0]
|
result = self.cached_result[0]
|
||||||
return result
|
return result
|
||||||
# we have a previous but differently parametrized fixture instance
|
# We have a previous but differently parametrized fixture instance
|
||||||
# so we need to tear it down before creating a new one
|
# so we need to tear it down before creating a new one.
|
||||||
self.finish(request)
|
self.finish(request)
|
||||||
assert self.cached_result is None
|
assert self.cached_result is None
|
||||||
|
|
||||||
|
@ -1073,21 +1076,20 @@ class FixtureDef(Generic[_FixtureValue]):
|
||||||
def resolve_fixture_function(
|
def resolve_fixture_function(
|
||||||
fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest
|
fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest
|
||||||
) -> "_FixtureFunc[_FixtureValue]":
|
) -> "_FixtureFunc[_FixtureValue]":
|
||||||
"""Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
|
"""Get the actual callable that can be called to obtain the fixture
|
||||||
instances and bound methods.
|
value, dealing with unittest-specific instances and bound methods."""
|
||||||
"""
|
|
||||||
fixturefunc = fixturedef.func
|
fixturefunc = fixturedef.func
|
||||||
if fixturedef.unittest:
|
if fixturedef.unittest:
|
||||||
if request.instance is not None:
|
if request.instance is not None:
|
||||||
# bind the unbound method to the TestCase instance
|
# Bind the unbound method to the TestCase instance.
|
||||||
fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
|
fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
|
||||||
else:
|
else:
|
||||||
# the fixture function needs to be bound to the actual
|
# The fixture function needs to be bound to the actual
|
||||||
# request.instance so that code working with "fixturedef" behaves
|
# request.instance so that code working with "fixturedef" behaves
|
||||||
# as expected.
|
# as expected.
|
||||||
if request.instance is not None:
|
if request.instance is not None:
|
||||||
# handle the case where fixture is defined not in a test class, but some other class
|
# Handle the case where fixture is defined not in a test class, but some other class
|
||||||
# (for example a plugin class with a fixture), see #2270
|
# (for example a plugin class with a fixture), see #2270.
|
||||||
if hasattr(fixturefunc, "__self__") and not isinstance(
|
if hasattr(fixturefunc, "__self__") and not isinstance(
|
||||||
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
|
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
|
||||||
):
|
):
|
||||||
|
@ -1101,7 +1103,7 @@ def resolve_fixture_function(
|
||||||
def pytest_fixture_setup(
|
def pytest_fixture_setup(
|
||||||
fixturedef: FixtureDef[_FixtureValue], request: SubRequest
|
fixturedef: FixtureDef[_FixtureValue], request: SubRequest
|
||||||
) -> _FixtureValue:
|
) -> _FixtureValue:
|
||||||
""" Execution of fixture setup. """
|
"""Execution of fixture setup."""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for argname in fixturedef.argnames:
|
for argname in fixturedef.argnames:
|
||||||
fixdef = request._get_active_fixturedef(argname)
|
fixdef = request._get_active_fixturedef(argname)
|
||||||
|
@ -1151,8 +1153,7 @@ def _params_converter(
|
||||||
|
|
||||||
def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
|
def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
|
||||||
"""Wrap the given fixture function so we can raise an error about it being called directly,
|
"""Wrap the given fixture function so we can raise an error about it being called directly,
|
||||||
instead of used as an argument in a test function.
|
instead of used as an argument in a test function."""
|
||||||
"""
|
|
||||||
message = (
|
message = (
|
||||||
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
|
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
|
||||||
"but are created automatically when test functions request them as parameters.\n"
|
"but are created automatically when test functions request them as parameters.\n"
|
||||||
|
@ -1164,8 +1165,8 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
|
||||||
def result(*args, **kwargs):
|
def result(*args, **kwargs):
|
||||||
fail(message, pytrace=False)
|
fail(message, pytrace=False)
|
||||||
|
|
||||||
# keep reference to the original function in our own custom attribute so we don't unwrap
|
# Keep reference to the original function in our own custom attribute so we don't unwrap
|
||||||
# further than this point and lose useful wrappings like @mock.patch (#3774)
|
# further than this point and lose useful wrappings like @mock.patch (#3774).
|
||||||
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
|
result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined]
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -1268,47 +1269,49 @@ def fixture( # noqa: F811
|
||||||
fixture function.
|
fixture function.
|
||||||
|
|
||||||
The name of the fixture function can later be referenced to cause its
|
The name of the fixture function can later be referenced to cause its
|
||||||
invocation ahead of running tests: test
|
invocation ahead of running tests: test modules or classes can use the
|
||||||
modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
|
``pytest.mark.usefixtures(fixturename)`` marker.
|
||||||
marker.
|
|
||||||
|
|
||||||
Test functions can directly use fixture names as input
|
Test functions can directly use fixture names as input arguments in which
|
||||||
arguments in which case the fixture instance returned from the fixture
|
case the fixture instance returned from the fixture function will be
|
||||||
function will be injected.
|
injected.
|
||||||
|
|
||||||
Fixtures can provide their values to test functions using ``return`` or ``yield``
|
Fixtures can provide their values to test functions using ``return`` or
|
||||||
statements. When using ``yield`` the code block after the ``yield`` statement is executed
|
``yield`` statements. When using ``yield`` the code block after the
|
||||||
as teardown code regardless of the test outcome, and must yield exactly once.
|
``yield`` statement is executed as teardown code regardless of the test
|
||||||
|
outcome, and must yield exactly once.
|
||||||
|
|
||||||
:arg scope: the scope for which this fixture is shared, one of
|
:param scope:
|
||||||
``"function"`` (default), ``"class"``, ``"module"``,
|
The scope for which this fixture is shared; one of ``"function"``
|
||||||
``"package"`` or ``"session"``.
|
(default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``.
|
||||||
|
|
||||||
This parameter may also be a callable which receives ``(fixture_name, config)``
|
This parameter may also be a callable which receives ``(fixture_name, config)``
|
||||||
as parameters, and must return a ``str`` with one of the values mentioned above.
|
as parameters, and must return a ``str`` with one of the values mentioned above.
|
||||||
|
|
||||||
See :ref:`dynamic scope` in the docs for more information.
|
See :ref:`dynamic scope` in the docs for more information.
|
||||||
|
|
||||||
:arg params: an optional list of parameters which will cause multiple
|
:param params:
|
||||||
invocations of the fixture function and all of the tests
|
An optional list of parameters which will cause multiple invocations
|
||||||
using it.
|
of the fixture function and all of the tests using it. The current
|
||||||
The current parameter is available in ``request.param``.
|
parameter is available in ``request.param``.
|
||||||
|
|
||||||
:arg autouse: if True, the fixture func is activated for all tests that
|
:param autouse:
|
||||||
can see it. If False (the default) then an explicit
|
If True, the fixture func is activated for all tests that can see it.
|
||||||
reference is needed to activate the fixture.
|
If False (the default), an explicit reference is needed to activate
|
||||||
|
the fixture.
|
||||||
|
|
||||||
:arg ids: list of string ids each corresponding to the params
|
:param ids:
|
||||||
so that they are part of the test id. If no ids are provided
|
List of string ids each corresponding to the params so that they are
|
||||||
they will be generated automatically from the params.
|
part of the test id. If no ids are provided they will be generated
|
||||||
|
automatically from the params.
|
||||||
|
|
||||||
:arg name: the name of the fixture. This defaults to the name of the
|
:param name:
|
||||||
decorated function. If a fixture is used in the same module in
|
The name of the fixture. This defaults to the name of the decorated
|
||||||
which it is defined, the function name of the fixture will be
|
function. If a fixture is used in the same module in which it is
|
||||||
shadowed by the function arg that requests the fixture; one way
|
defined, the function name of the fixture will be shadowed by the
|
||||||
to resolve this is to name the decorated function
|
function arg that requests the fixture; one way to resolve this is to
|
||||||
``fixture_<fixturename>`` and then use
|
name the decorated function ``fixture_<fixturename>`` and then use
|
||||||
``@pytest.fixture(name='<fixturename>')``.
|
``@pytest.fixture(name='<fixturename>')``.
|
||||||
"""
|
"""
|
||||||
# Positional arguments backward compatibility.
|
# Positional arguments backward compatibility.
|
||||||
# If a kwarg is equal to its default, assume it was not explicitly
|
# If a kwarg is equal to its default, assume it was not explicitly
|
||||||
|
@ -1377,7 +1380,7 @@ def yield_fixture(
|
||||||
ids=None,
|
ids=None,
|
||||||
name=None
|
name=None
|
||||||
):
|
):
|
||||||
""" (return a) decorator to mark a yield-fixture factory function.
|
"""(Return a) decorator to mark a yield-fixture factory function.
|
||||||
|
|
||||||
.. deprecated:: 3.0
|
.. deprecated:: 3.0
|
||||||
Use :py:func:`pytest.fixture` directly instead.
|
Use :py:func:`pytest.fixture` directly instead.
|
||||||
|
@ -1417,8 +1420,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
|
|
||||||
|
|
||||||
class FixtureManager:
|
class FixtureManager:
|
||||||
"""
|
"""pytest fixture definitions and information is stored and managed
|
||||||
pytest fixtures definitions and information is stored and managed
|
|
||||||
from this class.
|
from this class.
|
||||||
|
|
||||||
During collection fm.parsefactories() is called multiple times to parse
|
During collection fm.parsefactories() is called multiple times to parse
|
||||||
|
@ -1431,7 +1433,7 @@ class FixtureManager:
|
||||||
which themselves offer a fixturenames attribute.
|
which themselves offer a fixturenames attribute.
|
||||||
|
|
||||||
The FuncFixtureInfo object holds information about fixtures and FixtureDefs
|
The FuncFixtureInfo object holds information about fixtures and FixtureDefs
|
||||||
relevant for a particular function. An initial list of fixtures is
|
relevant for a particular function. An initial list of fixtures is
|
||||||
assembled like this:
|
assembled like this:
|
||||||
|
|
||||||
- ini-defined usefixtures
|
- ini-defined usefixtures
|
||||||
|
@ -1441,7 +1443,7 @@ class FixtureManager:
|
||||||
|
|
||||||
Subsequently the funcfixtureinfo.fixturenames attribute is computed
|
Subsequently the funcfixtureinfo.fixturenames attribute is computed
|
||||||
as the closure of the fixtures needed to setup the initial fixtures,
|
as the closure of the fixtures needed to setup the initial fixtures,
|
||||||
i. e. fixtures needed by fixture functions themselves are appended
|
i.e. fixtures needed by fixture functions themselves are appended
|
||||||
to the fixturenames list.
|
to the fixturenames list.
|
||||||
|
|
||||||
Upon the test-setup phases all fixturenames are instantiated, retrieved
|
Upon the test-setup phases all fixturenames are instantiated, retrieved
|
||||||
|
@ -1462,13 +1464,13 @@ class FixtureManager:
|
||||||
session.config.pluginmanager.register(self, "funcmanage")
|
session.config.pluginmanager.register(self, "funcmanage")
|
||||||
|
|
||||||
def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]:
|
def _get_direct_parametrize_args(self, node: "nodes.Node") -> List[str]:
|
||||||
"""This function returns all the direct parametrization
|
"""Return all direct parametrization arguments of a node, so we don't
|
||||||
arguments of a node, so we don't mistake them for fixtures
|
mistake them for fixtures.
|
||||||
|
|
||||||
Check https://github.com/pytest-dev/pytest/issues/5036
|
Check https://github.com/pytest-dev/pytest/issues/5036.
|
||||||
|
|
||||||
This things are done later as well when dealing with parametrization
|
These things are done later as well when dealing with parametrization
|
||||||
so this could be improved
|
so this could be improved.
|
||||||
"""
|
"""
|
||||||
parametrize_argnames = [] # type: List[str]
|
parametrize_argnames = [] # type: List[str]
|
||||||
for marker in node.iter_markers(name="parametrize"):
|
for marker in node.iter_markers(name="parametrize"):
|
||||||
|
@ -1507,9 +1509,9 @@ class FixtureManager:
|
||||||
else:
|
else:
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
|
|
||||||
# construct the base nodeid which is later used to check
|
# Construct the base nodeid which is later used to check
|
||||||
# what fixtures are visible for particular tests (as denoted
|
# what fixtures are visible for particular tests (as denoted
|
||||||
# by their test id)
|
# by their test id).
|
||||||
if p.basename.startswith("conftest.py"):
|
if p.basename.startswith("conftest.py"):
|
||||||
nodeid = p.dirpath().relto(self.config.rootdir)
|
nodeid = p.dirpath().relto(self.config.rootdir)
|
||||||
if p.sep != nodes.SEP:
|
if p.sep != nodes.SEP:
|
||||||
|
@ -1518,7 +1520,7 @@ class FixtureManager:
|
||||||
self.parsefactories(plugin, nodeid)
|
self.parsefactories(plugin, nodeid)
|
||||||
|
|
||||||
def _getautousenames(self, nodeid: str) -> List[str]:
|
def _getautousenames(self, nodeid: str) -> List[str]:
|
||||||
""" return a tuple of fixture names to be used. """
|
"""Return a list of fixture names to be used."""
|
||||||
autousenames = [] # type: List[str]
|
autousenames = [] # type: List[str]
|
||||||
for baseid, basenames in self._nodeid_and_autousenames:
|
for baseid, basenames in self._nodeid_and_autousenames:
|
||||||
if nodeid.startswith(baseid):
|
if nodeid.startswith(baseid):
|
||||||
|
@ -1533,12 +1535,12 @@ class FixtureManager:
|
||||||
def getfixtureclosure(
|
def getfixtureclosure(
|
||||||
self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = ()
|
self, fixturenames: Tuple[str, ...], parentnode, ignore_args: Sequence[str] = ()
|
||||||
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]:
|
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef]]]:
|
||||||
# collect the closure of all fixtures , starting with the given
|
# Collect the closure of all fixtures, starting with the given
|
||||||
# fixturenames as the initial set. As we have to visit all
|
# fixturenames as the initial set. As we have to visit all
|
||||||
# factory definitions anyway, we also return an arg2fixturedefs
|
# factory definitions anyway, we also return an arg2fixturedefs
|
||||||
# mapping so that the caller can reuse it and does not have
|
# mapping so that the caller can reuse it and does not have
|
||||||
# to re-discover fixturedefs again for each fixturename
|
# to re-discover fixturedefs again for each fixturename
|
||||||
# (discovering matching fixtures for a given name/node is expensive)
|
# (discovering matching fixtures for a given name/node is expensive).
|
||||||
|
|
||||||
parentid = parentnode.nodeid
|
parentid = parentnode.nodeid
|
||||||
fixturenames_closure = self._getautousenames(parentid)
|
fixturenames_closure = self._getautousenames(parentid)
|
||||||
|
@ -1550,7 +1552,7 @@ class FixtureManager:
|
||||||
|
|
||||||
merge(fixturenames)
|
merge(fixturenames)
|
||||||
|
|
||||||
# at this point, fixturenames_closure contains what we call "initialnames",
|
# At this point, fixturenames_closure contains what we call "initialnames",
|
||||||
# which is a set of fixturenames the function immediately requests. We
|
# which is a set of fixturenames the function immediately requests. We
|
||||||
# need to return it as well, so save this.
|
# need to return it as well, so save this.
|
||||||
initialnames = tuple(fixturenames_closure)
|
initialnames = tuple(fixturenames_closure)
|
||||||
|
@ -1608,10 +1610,10 @@ class FixtureManager:
|
||||||
ids=fixturedef.ids,
|
ids=fixturedef.ids,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
continue # will raise FixtureLookupError at setup time
|
continue # Will raise FixtureLookupError at setup time.
|
||||||
|
|
||||||
def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
|
def pytest_collection_modifyitems(self, items: "List[nodes.Item]") -> None:
|
||||||
# separate parametrized setups
|
# Separate parametrized setups.
|
||||||
items[:] = reorder_items(items)
|
items[:] = reorder_items(items)
|
||||||
|
|
||||||
def parsefactories(
|
def parsefactories(
|
||||||
|
@ -1633,16 +1635,17 @@ class FixtureManager:
|
||||||
obj = safe_getattr(holderobj, name, None)
|
obj = safe_getattr(holderobj, name, None)
|
||||||
marker = getfixturemarker(obj)
|
marker = getfixturemarker(obj)
|
||||||
if not isinstance(marker, FixtureFunctionMarker):
|
if not isinstance(marker, FixtureFunctionMarker):
|
||||||
# magic globals with __getattr__ might have got us a wrong
|
# Magic globals with __getattr__ might have got us a wrong
|
||||||
# fixture attribute
|
# fixture attribute.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if marker.name:
|
if marker.name:
|
||||||
name = marker.name
|
name = marker.name
|
||||||
|
|
||||||
# during fixture definition we wrap the original fixture function
|
# During fixture definition we wrap the original fixture function
|
||||||
# to issue a warning if called directly, so here we unwrap it in order to not emit the warning
|
# to issue a warning if called directly, so here we unwrap it in
|
||||||
# when pytest itself calls the fixture function
|
# order to not emit the warning when pytest itself calls the
|
||||||
|
# fixture function.
|
||||||
obj = get_real_method(obj, holderobj)
|
obj = get_real_method(obj, holderobj)
|
||||||
|
|
||||||
fixture_def = FixtureDef(
|
fixture_def = FixtureDef(
|
||||||
|
@ -1675,12 +1678,11 @@ class FixtureManager:
|
||||||
def getfixturedefs(
|
def getfixturedefs(
|
||||||
self, argname: str, nodeid: str
|
self, argname: str, nodeid: str
|
||||||
) -> Optional[Sequence[FixtureDef]]:
|
) -> Optional[Sequence[FixtureDef]]:
|
||||||
"""
|
"""Get a list of fixtures which are applicable to the given node id.
|
||||||
Gets a list of fixtures which are applicable to the given node id.
|
|
||||||
|
|
||||||
:param str argname: name of the fixture to search for
|
:param str argname: Name of the fixture to search for.
|
||||||
:param str nodeid: full node id of the requesting test.
|
:param str nodeid: Full node id of the requesting test.
|
||||||
:return: list[FixtureDef]
|
:rtype: Sequence[FixtureDef]
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fixturedefs = self._arg2fixturedefs[argname]
|
fixturedefs = self._arg2fixturedefs[argname]
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
"""
|
"""Provides a function to report all internal modules for using freezing
|
||||||
Provides a function to report all internal modules for using freezing tools
|
tools."""
|
||||||
pytest
|
|
||||||
"""
|
|
||||||
import types
|
import types
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -9,10 +7,8 @@ from typing import Union
|
||||||
|
|
||||||
|
|
||||||
def freeze_includes() -> List[str]:
|
def freeze_includes() -> List[str]:
|
||||||
"""
|
"""Return a list of module names used by pytest that should be
|
||||||
Returns a list of module names used by pytest that should be
|
included by cx_freeze."""
|
||||||
included by cx_freeze.
|
|
||||||
"""
|
|
||||||
import py
|
import py
|
||||||
import _pytest
|
import _pytest
|
||||||
|
|
||||||
|
@ -24,8 +20,7 @@ def freeze_includes() -> List[str]:
|
||||||
def _iter_all_modules(
|
def _iter_all_modules(
|
||||||
package: Union[str, types.ModuleType], prefix: str = "",
|
package: Union[str, types.ModuleType], prefix: str = "",
|
||||||
) -> Iterator[str]:
|
) -> Iterator[str]:
|
||||||
"""
|
"""Iterate over the names of all modules that can be found in the given
|
||||||
Iterates over the names of all modules that can be found in the given
|
|
||||||
package, recursively.
|
package, recursively.
|
||||||
|
|
||||||
>>> import _pytest
|
>>> import _pytest
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" version info, help messages, tracing configuration. """
|
"""Version info, help messages, tracing configuration."""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import Action
|
from argparse import Action
|
||||||
|
@ -16,8 +16,9 @@ from _pytest.config.argparsing import Parser
|
||||||
|
|
||||||
|
|
||||||
class HelpAction(Action):
|
class HelpAction(Action):
|
||||||
"""This is an argparse Action that will raise an exception in
|
"""An argparse Action that will raise an exception in order to skip the
|
||||||
order to skip the rest of the argument parsing when --help is passed.
|
rest of the argument parsing when --help is passed.
|
||||||
|
|
||||||
This prevents argparse from quitting due to missing required arguments
|
This prevents argparse from quitting due to missing required arguments
|
||||||
when any are defined, for example by ``pytest_addoption``.
|
when any are defined, for example by ``pytest_addoption``.
|
||||||
This is similar to the way that the builtin argparse --help option is
|
This is similar to the way that the builtin argparse --help option is
|
||||||
|
@ -37,7 +38,7 @@ class HelpAction(Action):
|
||||||
def __call__(self, parser, namespace, values, option_string=None):
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
setattr(namespace, self.dest, self.const)
|
setattr(namespace, self.dest, self.const)
|
||||||
|
|
||||||
# We should only skip the rest of the parsing after preparse is done
|
# We should only skip the rest of the parsing after preparse is done.
|
||||||
if getattr(parser._parser, "after_preparse", False):
|
if getattr(parser._parser, "after_preparse", False):
|
||||||
raise PrintHelp
|
raise PrintHelp
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
||||||
|
and by builtin plugins."""
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -51,11 +52,10 @@ hookspec = HookspecMarker("pytest")
|
||||||
|
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
||||||
"""called at plugin registration time to allow adding new hooks via a call to
|
"""Called at plugin registration time to allow adding new hooks via a call to
|
||||||
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
|
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
|
||||||
|
|
||||||
|
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager.
|
||||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with ``hookwrapper=True``.
|
This hook is incompatible with ``hookwrapper=True``.
|
||||||
|
@ -66,10 +66,10 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
||||||
def pytest_plugin_registered(
|
def pytest_plugin_registered(
|
||||||
plugin: "_PluggyPlugin", manager: "PytestPluginManager"
|
plugin: "_PluggyPlugin", manager: "PytestPluginManager"
|
||||||
) -> None:
|
) -> None:
|
||||||
""" a new pytest plugin got registered.
|
"""A new pytest plugin got registered.
|
||||||
|
|
||||||
:param plugin: the plugin module or instance
|
:param plugin: The plugin module or instance.
|
||||||
:param _pytest.config.PytestPluginManager manager: pytest plugin manager
|
:param _pytest.config.PytestPluginManager manager: pytest plugin manager.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with ``hookwrapper=True``.
|
This hook is incompatible with ``hookwrapper=True``.
|
||||||
|
@ -78,7 +78,7 @@ def pytest_plugin_registered(
|
||||||
|
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None:
|
def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None:
|
||||||
"""register argparse-style options and ini-style config values,
|
"""Register argparse-style options and ini-style config values,
|
||||||
called once at the beginning of a test run.
|
called once at the beginning of a test run.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
@ -87,15 +87,16 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
files situated at the tests root directory due to how pytest
|
files situated at the tests root directory due to how pytest
|
||||||
:ref:`discovers plugins during startup <pluginorder>`.
|
:ref:`discovers plugins during startup <pluginorder>`.
|
||||||
|
|
||||||
:arg _pytest.config.argparsing.Parser parser: To add command line options, call
|
:param _pytest.config.argparsing.Parser parser:
|
||||||
|
To add command line options, call
|
||||||
:py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`.
|
:py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`.
|
||||||
To add ini-file values call :py:func:`parser.addini(...)
|
To add ini-file values call :py:func:`parser.addini(...)
|
||||||
<_pytest.config.argparsing.Parser.addini>`.
|
<_pytest.config.argparsing.Parser.addini>`.
|
||||||
|
|
||||||
:arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager,
|
:param _pytest.config.PytestPluginManager pluginmanager:
|
||||||
which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s
|
pytest plugin manager, which can be used to install :py:func:`hookspec`'s
|
||||||
and allow one plugin to call another plugin's hooks to change how
|
or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks
|
||||||
command line options are added.
|
to change how command line options are added.
|
||||||
|
|
||||||
Options can later be accessed through the
|
Options can later be accessed through the
|
||||||
:py:class:`config <_pytest.config.Config>` object, respectively:
|
:py:class:`config <_pytest.config.Config>` object, respectively:
|
||||||
|
@ -116,8 +117,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
|
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_configure(config: "Config") -> None:
|
def pytest_configure(config: "Config") -> None:
|
||||||
"""
|
"""Allow plugins and conftest files to perform initial configuration.
|
||||||
Allows plugins and conftest files to perform initial configuration.
|
|
||||||
|
|
||||||
This hook is called for every plugin and initial conftest file
|
This hook is called for every plugin and initial conftest file
|
||||||
after command line options have been parsed.
|
after command line options have been parsed.
|
||||||
|
@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None:
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with ``hookwrapper=True``.
|
This hook is incompatible with ``hookwrapper=True``.
|
||||||
|
|
||||||
:arg _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,16 +142,17 @@ def pytest_configure(config: "Config") -> None:
|
||||||
def pytest_cmdline_parse(
|
def pytest_cmdline_parse(
|
||||||
pluginmanager: "PytestPluginManager", args: List[str]
|
pluginmanager: "PytestPluginManager", args: List[str]
|
||||||
) -> Optional["Config"]:
|
) -> Optional["Config"]:
|
||||||
"""return initialized config object, parsing the specified args.
|
"""Return an initialized config object, parsing the specified args.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook will only be called for plugin classes passed to the ``plugins`` arg when using `pytest.main`_ to
|
This hook will only be called for plugin classes passed to the
|
||||||
perform an in-process test run.
|
``plugins`` arg when using `pytest.main`_ to perform an in-process
|
||||||
|
test run.
|
||||||
|
|
||||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
:param _pytest.config.PytestPluginManager pluginmanager: Pytest plugin manager.
|
||||||
:param list[str] args: list of arguments passed on the command line
|
:param List[str] args: List of arguments passed on the command line.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,37 +165,37 @@ def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
|
||||||
.. note::
|
.. note::
|
||||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
:param list[str] args: list of arguments passed on the command line
|
:param List[str] args: Arguments passed on the command line.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
||||||
""" called for performing the main command line action. The default
|
"""Called for performing the main command line action. The default
|
||||||
implementation will invoke the configure hooks and runtest_mainloop.
|
implementation will invoke the configure hooks and runtest_mainloop.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_load_initial_conftests(
|
def pytest_load_initial_conftests(
|
||||||
early_config: "Config", parser: "Parser", args: List[str]
|
early_config: "Config", parser: "Parser", args: List[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
""" implements the loading of initial conftest files ahead
|
"""Called to implement the loading of initial conftest files ahead
|
||||||
of command line option parsing.
|
of command line option parsing.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||||
|
|
||||||
:param _pytest.config.Config early_config: pytest config object
|
:param _pytest.config.Config early_config: The pytest config object.
|
||||||
:param list[str] args: list of arguments passed on the command line
|
:param List[str] args: Arguments passed on the command line.
|
||||||
:param _pytest.config.argparsing.Parser parser: to add command line options
|
:param _pytest.config.argparsing.Parser parser: To add command line options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -224,26 +225,26 @@ def pytest_collection(session: "Session") -> Optional[object]:
|
||||||
for example the terminal plugin uses it to start displaying the collection
|
for example the terminal plugin uses it to start displaying the collection
|
||||||
counter (and returns `None`).
|
counter (and returns `None`).
|
||||||
|
|
||||||
:param _pytest.main.Session session: the pytest session object
|
:param _pytest.main.Session session: The pytest session object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_collection_modifyitems(
|
def pytest_collection_modifyitems(
|
||||||
session: "Session", config: "Config", items: List["Item"]
|
session: "Session", config: "Config", items: List["Item"]
|
||||||
) -> None:
|
) -> None:
|
||||||
""" called after collection has been performed, may filter or re-order
|
"""Called after collection has been performed. May filter or re-order
|
||||||
the items in-place.
|
the items in-place.
|
||||||
|
|
||||||
:param _pytest.main.Session session: the pytest session object
|
:param _pytest.main.Session session: The pytest session object.
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
:param List[_pytest.nodes.Item] items: list of item objects
|
:param List[_pytest.nodes.Item] items: List of item objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_collection_finish(session: "Session") -> None:
|
def pytest_collection_finish(session: "Session") -> None:
|
||||||
"""Called after collection has been performed and modified.
|
"""Called after collection has been performed and modified.
|
||||||
|
|
||||||
:param _pytest.main.Session session: the pytest session object
|
:param _pytest.main.Session session: The pytest session object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -256,8 +257,8 @@ def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[boo
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
:param path: a :py:class:`py.path.local` - the path to analyze
|
:param py.path.local path: The path to analyze.
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -267,7 +268,7 @@ def pytest_collect_directory(path: py.path.local, parent) -> Optional[object]:
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
:param path: a :py:class:`py.path.local` - the path to analyze
|
:param py.path.local path: The path to analyze.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -276,7 +277,7 @@ def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]":
|
||||||
|
|
||||||
Any new node needs to have the specified ``parent`` as a parent.
|
Any new node needs to have the specified ``parent`` as a parent.
|
||||||
|
|
||||||
:param path: a :py:class:`py.path.local` - the path to collect
|
:param py.path.local path: The path to collect.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,7 +285,7 @@ def pytest_collect_file(path: py.path.local, parent) -> "Optional[Collector]":
|
||||||
|
|
||||||
|
|
||||||
def pytest_collectstart(collector: "Collector") -> None:
|
def pytest_collectstart(collector: "Collector") -> None:
|
||||||
""" collector starts collecting. """
|
"""Collector starts collecting."""
|
||||||
|
|
||||||
|
|
||||||
def pytest_itemcollected(item: "Item") -> None:
|
def pytest_itemcollected(item: "Item") -> None:
|
||||||
|
@ -292,7 +293,7 @@ def pytest_itemcollected(item: "Item") -> None:
|
||||||
|
|
||||||
|
|
||||||
def pytest_collectreport(report: "CollectReport") -> None:
|
def pytest_collectreport(report: "CollectReport") -> None:
|
||||||
""" collector finished collecting. """
|
"""Collector finished collecting."""
|
||||||
|
|
||||||
|
|
||||||
def pytest_deselected(items: Sequence["Item"]) -> None:
|
def pytest_deselected(items: Sequence["Item"]) -> None:
|
||||||
|
@ -301,9 +302,10 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]":
|
def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]":
|
||||||
""" perform ``collector.collect()`` and return a CollectReport.
|
"""Perform ``collector.collect()`` and return a CollectReport.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult` """
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
@ -321,7 +323,7 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
:param path: a :py:class:`py.path.local` - the path of module to collect
|
:param py.path.local path: The path of module to collect.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -337,28 +339,31 @@ def pytest_pycollect_makeitem(
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
|
def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
|
||||||
""" call underlying test function.
|
"""Call underlying test function.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult` """
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
||||||
""" generate (multiple) parametrized calls to a test function."""
|
"""Generate (multiple) parametrized calls to a test function."""
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_make_parametrize_id(
|
def pytest_make_parametrize_id(
|
||||||
config: "Config", val: object, argname: str
|
config: "Config", val: object, argname: str
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Return a user-friendly string representation of the given ``val`` that will be used
|
"""Return a user-friendly string representation of the given ``val``
|
||||||
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
|
that will be used by @pytest.mark.parametrize calls, or None if the hook
|
||||||
|
doesn't know about ``val``.
|
||||||
|
|
||||||
The parameter name is available as ``argname``, if required.
|
The parameter name is available as ``argname``, if required.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
:param val: the parametrized value
|
:param val: The parametrized value.
|
||||||
:param str argname: the automatic parameter name produced by pytest
|
:param str argname: The automatic parameter name produced by pytest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -369,7 +374,7 @@ def pytest_make_parametrize_id(
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_runtestloop(session: "Session") -> Optional[object]:
|
def pytest_runtestloop(session: "Session") -> Optional[object]:
|
||||||
"""Performs the main runtest loop (after collection finished).
|
"""Perform the main runtest loop (after collection finished).
|
||||||
|
|
||||||
The default hook implementation performs the runtest protocol for all items
|
The default hook implementation performs the runtest protocol for all items
|
||||||
collected in the session (``session.items``), unless the collection failed
|
collected in the session (``session.items``), unless the collection failed
|
||||||
|
@ -392,7 +397,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
|
||||||
def pytest_runtest_protocol(
|
def pytest_runtest_protocol(
|
||||||
item: "Item", nextitem: "Optional[Item]"
|
item: "Item", nextitem: "Optional[Item]"
|
||||||
) -> Optional[object]:
|
) -> Optional[object]:
|
||||||
"""Performs the runtest protocol for a single test item.
|
"""Perform the runtest protocol for a single test item.
|
||||||
|
|
||||||
The default runtest protocol is this (see individual hooks for full details):
|
The default runtest protocol is this (see individual hooks for full details):
|
||||||
|
|
||||||
|
@ -418,9 +423,8 @@ def pytest_runtest_protocol(
|
||||||
|
|
||||||
- ``pytest_runtest_logfinish(nodeid, location)``
|
- ``pytest_runtest_logfinish(nodeid, location)``
|
||||||
|
|
||||||
:arg item: Test item for which the runtest protocol is performed.
|
:param item: Test item for which the runtest protocol is performed.
|
||||||
|
:param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
|
||||||
:arg nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
|
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
The return value is not used, but only stops further processing.
|
The return value is not used, but only stops further processing.
|
||||||
|
@ -476,10 +480,11 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
|
||||||
includes running the teardown phase of fixtures required by the item (if
|
includes running the teardown phase of fixtures required by the item (if
|
||||||
they go out of scope).
|
they go out of scope).
|
||||||
|
|
||||||
:arg nextitem: The scheduled-to-be-next test item (None if no further
|
:param nextitem:
|
||||||
test item is scheduled). This argument can be used to
|
The scheduled-to-be-next test item (None if no further test item is
|
||||||
perform exact teardowns, i.e. calling just enough finalizers
|
scheduled). This argument can be used to perform exact teardowns,
|
||||||
so that nextitem only needs to call setup-functions.
|
i.e. calling just enough finalizers so that nextitem only needs to
|
||||||
|
call setup-functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -510,19 +515,15 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
|
||||||
def pytest_report_to_serializable(
|
def pytest_report_to_serializable(
|
||||||
config: "Config", report: Union["CollectReport", "TestReport"],
|
config: "Config", report: Union["CollectReport", "TestReport"],
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""Serialize the given report object into a data structure suitable for
|
||||||
Serializes the given report object into a data structure suitable for sending
|
sending over the wire, e.g. converted to JSON."""
|
||||||
over the wire, e.g. converted to JSON.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_report_from_serializable(
|
def pytest_report_from_serializable(
|
||||||
config: "Config", data: Dict[str, Any],
|
config: "Config", data: Dict[str, Any],
|
||||||
) -> Optional[Union["CollectReport", "TestReport"]]:
|
) -> Optional[Union["CollectReport", "TestReport"]]:
|
||||||
"""
|
"""Restore a report object previously serialized with pytest_report_to_serializable()."""
|
||||||
Restores a report object previously serialized with pytest_report_to_serializable().
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
@ -534,9 +535,9 @@ def pytest_report_from_serializable(
|
||||||
def pytest_fixture_setup(
|
def pytest_fixture_setup(
|
||||||
fixturedef: "FixtureDef", request: "SubRequest"
|
fixturedef: "FixtureDef", request: "SubRequest"
|
||||||
) -> Optional[object]:
|
) -> Optional[object]:
|
||||||
"""Performs fixture setup execution.
|
"""Perform fixture setup execution.
|
||||||
|
|
||||||
:return: The return value of the call to the fixture function.
|
:returns: The return value of the call to the fixture function.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
|
@ -564,7 +565,7 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||||
"""Called after the ``Session`` object has been created and before performing collection
|
"""Called after the ``Session`` object has been created and before performing collection
|
||||||
and entering the run test loop.
|
and entering the run test loop.
|
||||||
|
|
||||||
:param _pytest.main.Session session: the pytest session object
|
:param _pytest.main.Session session: The pytest session object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -573,15 +574,15 @@ def pytest_sessionfinish(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Called after whole test run finished, right before returning the exit status to the system.
|
"""Called after whole test run finished, right before returning the exit status to the system.
|
||||||
|
|
||||||
:param _pytest.main.Session session: the pytest session object
|
:param _pytest.main.Session session: The pytest session object.
|
||||||
:param int exitstatus: the status which pytest will return to the system
|
:param int exitstatus: The status which pytest will return to the system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config: "Config") -> None:
|
def pytest_unconfigure(config: "Config") -> None:
|
||||||
"""Called before test process is exited.
|
"""Called before test process is exited.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -596,22 +597,19 @@ def pytest_assertrepr_compare(
|
||||||
"""Return explanation for comparisons in failing assert expressions.
|
"""Return explanation for comparisons in failing assert expressions.
|
||||||
|
|
||||||
Return None for no custom explanation, otherwise return a list
|
Return None for no custom explanation, otherwise return a list
|
||||||
of strings. The strings will be joined by newlines but any newlines
|
of strings. The strings will be joined by newlines but any newlines
|
||||||
*in* a string will be escaped. Note that all but the first line will
|
*in* a string will be escaped. Note that all but the first line will
|
||||||
be indented slightly, the intention is for the first line to be a summary.
|
be indented slightly, the intention is for the first line to be a summary.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None:
|
def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None:
|
||||||
"""
|
"""**(Experimental)** Called whenever an assertion passes.
|
||||||
**(Experimental)**
|
|
||||||
|
|
||||||
.. versionadded:: 5.0
|
.. versionadded:: 5.0
|
||||||
|
|
||||||
Hook called whenever an assertion *passes*.
|
|
||||||
|
|
||||||
Use this hook to do some processing after a passing assertion.
|
Use this hook to do some processing after a passing assertion.
|
||||||
The original assertion information is available in the `orig` string
|
The original assertion information is available in the `orig` string
|
||||||
and the pytest introspected assertion information is available in the
|
and the pytest introspected assertion information is available in the
|
||||||
|
@ -628,32 +626,32 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||||
You need to **clean the .pyc** files in your project directory and interpreter libraries
|
You need to **clean the .pyc** files in your project directory and interpreter libraries
|
||||||
when enabling this option, as assertions will require to be re-written.
|
when enabling this option, as assertions will require to be re-written.
|
||||||
|
|
||||||
:param _pytest.nodes.Item item: pytest item object of current test
|
:param _pytest.nodes.Item item: pytest item object of current test.
|
||||||
:param int lineno: line number of the assert statement
|
:param int lineno: Line number of the assert statement.
|
||||||
:param string orig: string with original assertion
|
:param str orig: String with the original assertion.
|
||||||
:param string expl: string with assert explanation
|
:param str expl: String with the assert explanation.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This hook is **experimental**, so its parameters or even the hook itself might
|
This hook is **experimental**, so its parameters or even the hook itself might
|
||||||
be changed/removed without warning in any future pytest release.
|
be changed/removed without warning in any future pytest release.
|
||||||
|
|
||||||
If you find this hook useful, please share your feedback opening an issue.
|
If you find this hook useful, please share your feedback in an issue.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# hooks for influencing reporting (invoked from _pytest_terminal)
|
# Hooks for influencing reporting (invoked from _pytest_terminal).
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header(
|
def pytest_report_header(
|
||||||
config: "Config", startdir: py.path.local
|
config: "Config", startdir: py.path.local
|
||||||
) -> Union[str, List[str]]:
|
) -> Union[str, List[str]]:
|
||||||
""" return a string or list of strings to be displayed as header info for terminal reporting.
|
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
:param startdir: py.path object with the starting dir
|
:param py.path.local startdir: The starting dir.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -673,16 +671,16 @@ def pytest_report_header(
|
||||||
def pytest_report_collectionfinish(
|
def pytest_report_collectionfinish(
|
||||||
config: "Config", startdir: py.path.local, items: Sequence["Item"],
|
config: "Config", startdir: py.path.local, items: Sequence["Item"],
|
||||||
) -> Union[str, List[str]]:
|
) -> Union[str, List[str]]:
|
||||||
"""
|
"""Return a string or list of strings to be displayed after collection
|
||||||
.. versionadded:: 3.2
|
has finished successfully.
|
||||||
|
|
||||||
Return a string or list of strings to be displayed after collection has finished successfully.
|
|
||||||
|
|
||||||
These strings will be displayed after the standard "collected X items" message.
|
These strings will be displayed after the standard "collected X items" message.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
.. versionadded:: 3.2
|
||||||
:param startdir: py.path object with the starting dir
|
|
||||||
:param items: list of pytest items that are going to be executed; this list should not be modified.
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
|
:param py.path.local startdir: The starting dir.
|
||||||
|
:param items: List of pytest items that are going to be executed; this list should not be modified.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -727,9 +725,9 @@ def pytest_terminal_summary(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a section to terminal summary reporting.
|
"""Add a section to terminal summary reporting.
|
||||||
|
|
||||||
:param _pytest.terminal.TerminalReporter terminalreporter: the internal terminal reporter object
|
:param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object.
|
||||||
:param int exitstatus: the exit status that will be reported back to the OS
|
:param int exitstatus: The exit status that will be reported back to the OS.
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
|
|
||||||
.. versionadded:: 4.2
|
.. versionadded:: 4.2
|
||||||
The ``config`` parameter.
|
The ``config`` parameter.
|
||||||
|
@ -780,8 +778,7 @@ def pytest_warning_recorded(
|
||||||
nodeid: str,
|
nodeid: str,
|
||||||
location: Optional[Tuple[str, int, str]],
|
location: Optional[Tuple[str, int, str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Process a warning captured by the internal pytest warnings plugin.
|
||||||
Process a warning captured by the internal pytest warnings plugin.
|
|
||||||
|
|
||||||
:param warnings.WarningMessage warning_message:
|
:param warnings.WarningMessage warning_message:
|
||||||
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
||||||
|
@ -794,7 +791,8 @@ def pytest_warning_recorded(
|
||||||
* ``"collect"``: during test collection.
|
* ``"collect"``: during test collection.
|
||||||
* ``"runtest"``: during test execution.
|
* ``"runtest"``: during test execution.
|
||||||
|
|
||||||
:param str nodeid: full id of the item
|
:param str nodeid:
|
||||||
|
Full id of the item.
|
||||||
|
|
||||||
:param tuple|None location:
|
:param tuple|None location:
|
||||||
When available, holds information about the execution context of the captured
|
When available, holds information about the execution context of the captured
|
||||||
|
@ -823,7 +821,7 @@ def pytest_internalerror(
|
||||||
def pytest_keyboard_interrupt(
|
def pytest_keyboard_interrupt(
|
||||||
excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
|
excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
|
||||||
) -> None:
|
) -> None:
|
||||||
""" called for keyboard interrupt. """
|
"""Called for keyboard interrupt."""
|
||||||
|
|
||||||
|
|
||||||
def pytest_exception_interact(
|
def pytest_exception_interact(
|
||||||
|
@ -846,20 +844,22 @@ def pytest_exception_interact(
|
||||||
|
|
||||||
|
|
||||||
def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
||||||
""" called upon pdb.set_trace(), can be used by plugins to take special
|
"""Called upon pdb.set_trace().
|
||||||
action just before the python debugger enters in interactive mode.
|
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
Can be used by plugins to take special action just before the python
|
||||||
:param pdb.Pdb pdb: Pdb instance
|
debugger enters interactive mode.
|
||||||
|
|
||||||
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
|
:param pdb.Pdb pdb: The Pdb instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
||||||
""" called when leaving pdb (e.g. with continue after pdb.set_trace()).
|
"""Called when leaving pdb (e.g. with continue after pdb.set_trace()).
|
||||||
|
|
||||||
Can be used by plugins to take special action just after the python
|
Can be used by plugins to take special action just after the python
|
||||||
debugger leaves interactive mode.
|
debugger leaves interactive mode.
|
||||||
|
|
||||||
:param _pytest.config.Config config: pytest config object
|
:param _pytest.config.Config config: The pytest config object.
|
||||||
:param pdb.Pdb pdb: Pdb instance
|
:param pdb.Pdb pdb: The Pdb instance.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
"""
|
"""Report test results in JUnit-XML format, for use with Jenkins and build
|
||||||
report test results in JUnit-XML format,
|
integration servers.
|
||||||
for use with Jenkins and build integration servers.
|
|
||||||
|
|
||||||
|
|
||||||
Based on initial code from Ross Lawley.
|
Based on initial code from Ross Lawley.
|
||||||
|
|
||||||
Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
|
Output conforms to
|
||||||
src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||||
"""
|
"""
|
||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
|
@ -81,11 +79,11 @@ families = {}
|
||||||
families["_base"] = {"testcase": ["classname", "name"]}
|
families["_base"] = {"testcase": ["classname", "name"]}
|
||||||
families["_base_legacy"] = {"testcase": ["file", "line", "url"]}
|
families["_base_legacy"] = {"testcase": ["file", "line", "url"]}
|
||||||
|
|
||||||
# xUnit 1.x inherits legacy attributes
|
# xUnit 1.x inherits legacy attributes.
|
||||||
families["xunit1"] = families["_base"].copy()
|
families["xunit1"] = families["_base"].copy()
|
||||||
merge_family(families["xunit1"], families["_base_legacy"])
|
merge_family(families["xunit1"], families["_base_legacy"])
|
||||||
|
|
||||||
# xUnit 2.x uses strict base attributes
|
# xUnit 2.x uses strict base attributes.
|
||||||
families["xunit2"] = families["_base"]
|
families["xunit2"] = families["_base"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -111,8 +109,7 @@ class _NodeReporter:
|
||||||
self.attrs[str(name)] = bin_xml_escape(value)
|
self.attrs[str(name)] = bin_xml_escape(value)
|
||||||
|
|
||||||
def make_properties_node(self) -> Optional[ET.Element]:
|
def make_properties_node(self) -> Optional[ET.Element]:
|
||||||
"""Return a Junit node containing custom properties, if any.
|
"""Return a Junit node containing custom properties, if any."""
|
||||||
"""
|
|
||||||
if self.properties:
|
if self.properties:
|
||||||
properties = ET.Element("properties")
|
properties = ET.Element("properties")
|
||||||
for name, value in self.properties:
|
for name, value in self.properties:
|
||||||
|
@ -136,9 +133,9 @@ class _NodeReporter:
|
||||||
if hasattr(testreport, "url"):
|
if hasattr(testreport, "url"):
|
||||||
attrs["url"] = testreport.url
|
attrs["url"] = testreport.url
|
||||||
self.attrs = attrs
|
self.attrs = attrs
|
||||||
self.attrs.update(existing_attrs) # restore any user-defined attributes
|
self.attrs.update(existing_attrs) # Restore any user-defined attributes.
|
||||||
|
|
||||||
# Preserve legacy testcase behavior
|
# Preserve legacy testcase behavior.
|
||||||
if self.family == "xunit1":
|
if self.family == "xunit1":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -262,7 +259,7 @@ class _NodeReporter:
|
||||||
def _warn_incompatibility_with_xunit2(
|
def _warn_incompatibility_with_xunit2(
|
||||||
request: FixtureRequest, fixture_name: str
|
request: FixtureRequest, fixture_name: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions"""
|
"""Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
xml = request.config._store.get(xml_key, None)
|
xml = request.config._store.get(xml_key, None)
|
||||||
|
@ -330,7 +327,7 @@ def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], Non
|
||||||
|
|
||||||
def _check_record_param_type(param: str, v: str) -> None:
|
def _check_record_param_type(param: str, v: str) -> None:
|
||||||
"""Used by record_testsuite_property to check that the given parameter name is of the proper
|
"""Used by record_testsuite_property to check that the given parameter name is of the proper
|
||||||
type"""
|
type."""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
if not isinstance(v, str):
|
if not isinstance(v, str):
|
||||||
msg = "{param} parameter needs to be a string, but {g} given"
|
msg = "{param} parameter needs to be a string, but {g} given"
|
||||||
|
@ -339,9 +336,10 @@ def _check_record_param_type(param: str, v: str) -> None:
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
|
def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
|
||||||
"""
|
"""Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
||||||
Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
|
|
||||||
writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
|
This is suitable to writing global information regarding the entire test
|
||||||
|
suite, and is compatible with ``xunit2`` JUnit family.
|
||||||
|
|
||||||
This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
|
This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
|
||||||
|
|
||||||
|
@ -357,7 +355,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
def record_func(name: str, value: object) -> None:
|
def record_func(name: str, value: object) -> None:
|
||||||
"""noop function in case --junitxml was not passed in the command-line"""
|
"""No-op function in case --junitxml was not passed in the command-line."""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
_check_record_param_type("name", name)
|
_check_record_param_type("name", name)
|
||||||
|
|
||||||
|
@ -414,7 +412,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
|
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
xmlpath = config.option.xmlpath
|
xmlpath = config.option.xmlpath
|
||||||
# prevent opening xmllog on worker nodes (xdist)
|
# Prevent opening xmllog on worker nodes (xdist).
|
||||||
if xmlpath and not hasattr(config, "workerinput"):
|
if xmlpath and not hasattr(config, "workerinput"):
|
||||||
junit_family = config.getini("junit_family")
|
junit_family = config.getini("junit_family")
|
||||||
if not junit_family:
|
if not junit_family:
|
||||||
|
@ -446,10 +444,10 @@ def mangle_test_address(address: str) -> List[str]:
|
||||||
names.remove("()")
|
names.remove("()")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
# convert file path to dotted path
|
# Convert file path to dotted path.
|
||||||
names[0] = names[0].replace(nodes.SEP, ".")
|
names[0] = names[0].replace(nodes.SEP, ".")
|
||||||
names[0] = re.sub(r"\.py$", "", names[0])
|
names[0] = re.sub(r"\.py$", "", names[0])
|
||||||
# put any params back
|
# Put any params back.
|
||||||
names[-1] += possible_open_bracket + params
|
names[-1] += possible_open_bracket + params
|
||||||
return names
|
return names
|
||||||
|
|
||||||
|
@ -486,13 +484,13 @@ class LogXML:
|
||||||
self.open_reports = [] # type: List[TestReport]
|
self.open_reports = [] # type: List[TestReport]
|
||||||
self.cnt_double_fail_tests = 0
|
self.cnt_double_fail_tests = 0
|
||||||
|
|
||||||
# Replaces convenience family with real family
|
# Replaces convenience family with real family.
|
||||||
if self.family == "legacy":
|
if self.family == "legacy":
|
||||||
self.family = "xunit1"
|
self.family = "xunit1"
|
||||||
|
|
||||||
def finalize(self, report: TestReport) -> None:
|
def finalize(self, report: TestReport) -> None:
|
||||||
nodeid = getattr(report, "nodeid", report)
|
nodeid = getattr(report, "nodeid", report)
|
||||||
# local hack to handle xdist report order
|
# Local hack to handle xdist report order.
|
||||||
workernode = getattr(report, "node", None)
|
workernode = getattr(report, "node", None)
|
||||||
reporter = self.node_reporters.pop((nodeid, workernode))
|
reporter = self.node_reporters.pop((nodeid, workernode))
|
||||||
if reporter is not None:
|
if reporter is not None:
|
||||||
|
@ -500,7 +498,7 @@ class LogXML:
|
||||||
|
|
||||||
def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
|
def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
|
||||||
nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport]
|
nodeid = getattr(report, "nodeid", report) # type: Union[str, TestReport]
|
||||||
# local hack to handle xdist report order
|
# Local hack to handle xdist report order.
|
||||||
workernode = getattr(report, "node", None)
|
workernode = getattr(report, "node", None)
|
||||||
|
|
||||||
key = nodeid, workernode
|
key = nodeid, workernode
|
||||||
|
@ -526,13 +524,13 @@ class LogXML:
|
||||||
return reporter
|
return reporter
|
||||||
|
|
||||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||||
"""handle a setup/call/teardown report, generating the appropriate
|
"""Handle a setup/call/teardown report, generating the appropriate
|
||||||
xml tags as necessary.
|
XML tags as necessary.
|
||||||
|
|
||||||
note: due to plugins like xdist, this hook may be called in interlaced
|
Note: due to plugins like xdist, this hook may be called in interlaced
|
||||||
order with reports from other nodes. for example:
|
order with reports from other nodes. For example:
|
||||||
|
|
||||||
usual call order:
|
Usual call order:
|
||||||
-> setup node1
|
-> setup node1
|
||||||
-> call node1
|
-> call node1
|
||||||
-> teardown node1
|
-> teardown node1
|
||||||
|
@ -540,7 +538,7 @@ class LogXML:
|
||||||
-> call node2
|
-> call node2
|
||||||
-> teardown node2
|
-> teardown node2
|
||||||
|
|
||||||
possible call order in xdist:
|
Possible call order in xdist:
|
||||||
-> setup node1
|
-> setup node1
|
||||||
-> call node1
|
-> call node1
|
||||||
-> setup node2
|
-> setup node2
|
||||||
|
@ -555,7 +553,7 @@ class LogXML:
|
||||||
reporter.append_pass(report)
|
reporter.append_pass(report)
|
||||||
elif report.failed:
|
elif report.failed:
|
||||||
if report.when == "teardown":
|
if report.when == "teardown":
|
||||||
# The following vars are needed when xdist plugin is used
|
# The following vars are needed when xdist plugin is used.
|
||||||
report_wid = getattr(report, "worker_id", None)
|
report_wid = getattr(report, "worker_id", None)
|
||||||
report_ii = getattr(report, "item_index", None)
|
report_ii = getattr(report, "item_index", None)
|
||||||
close_report = next(
|
close_report = next(
|
||||||
|
@ -573,7 +571,7 @@ class LogXML:
|
||||||
if close_report:
|
if close_report:
|
||||||
# We need to open new testcase in case we have failure in
|
# We need to open new testcase in case we have failure in
|
||||||
# call and error in teardown in order to follow junit
|
# call and error in teardown in order to follow junit
|
||||||
# schema
|
# schema.
|
||||||
self.finalize(close_report)
|
self.finalize(close_report)
|
||||||
self.cnt_double_fail_tests += 1
|
self.cnt_double_fail_tests += 1
|
||||||
reporter = self._opentestcase(report)
|
reporter = self._opentestcase(report)
|
||||||
|
@ -614,9 +612,8 @@ class LogXML:
|
||||||
self.open_reports.remove(close_report)
|
self.open_reports.remove(close_report)
|
||||||
|
|
||||||
def update_testcase_duration(self, report: TestReport) -> None:
|
def update_testcase_duration(self, report: TestReport) -> None:
|
||||||
"""accumulates total duration for nodeid from given report and updates
|
"""Accumulate total duration for nodeid from given report and update
|
||||||
the Junit.testcase with the new total if already created.
|
the Junit.testcase with the new total if already created."""
|
||||||
"""
|
|
||||||
if self.report_duration == "total" or report.when == self.report_duration:
|
if self.report_duration == "total" or report.when == self.report_duration:
|
||||||
reporter = self.node_reporter(report)
|
reporter = self.node_reporter(report)
|
||||||
reporter.duration += getattr(report, "duration", 0.0)
|
reporter.duration += getattr(report, "duration", 0.0)
|
||||||
|
@ -684,8 +681,7 @@ class LogXML:
|
||||||
self.global_properties.append((name, bin_xml_escape(value)))
|
self.global_properties.append((name, bin_xml_escape(value)))
|
||||||
|
|
||||||
def _get_global_properties_node(self) -> Optional[ET.Element]:
|
def _get_global_properties_node(self) -> Optional[ET.Element]:
|
||||||
"""Return a Junit node containing custom properties, if any.
|
"""Return a Junit node containing custom properties, if any."""
|
||||||
"""
|
|
||||||
if self.global_properties:
|
if self.global_properties:
|
||||||
properties = ET.Element("properties")
|
properties = ET.Element("properties")
|
||||||
for name, value in self.global_properties:
|
for name, value in self.global_properties:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" Access and control log capturing. """
|
"""Access and control log capturing."""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -43,9 +43,8 @@ def _remove_ansi_escape_sequences(text: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
class ColoredLevelFormatter(logging.Formatter):
|
class ColoredLevelFormatter(logging.Formatter):
|
||||||
"""
|
"""A logging formatter which colorizes the %(levelname)..s part of the
|
||||||
Colorize the %(levelname)..s part of the log format passed to __init__.
|
log format passed to __init__."""
|
||||||
"""
|
|
||||||
|
|
||||||
LOGLEVEL_COLOROPTS = {
|
LOGLEVEL_COLOROPTS = {
|
||||||
logging.CRITICAL: {"red"},
|
logging.CRITICAL: {"red"},
|
||||||
|
@ -110,7 +109,7 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int:
|
def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int:
|
||||||
"""Determines the current auto indentation setting
|
"""Determine the current auto indentation setting.
|
||||||
|
|
||||||
Specify auto indent behavior (on/off/fixed) by passing in
|
Specify auto indent behavior (on/off/fixed) by passing in
|
||||||
extra={"auto_indent": [value]} to the call to logging.log() or
|
extra={"auto_indent": [value]} to the call to logging.log() or
|
||||||
|
@ -128,12 +127,14 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||||
Any other values for the option are invalid, and will silently be
|
Any other values for the option are invalid, and will silently be
|
||||||
converted to the default.
|
converted to the default.
|
||||||
|
|
||||||
:param any auto_indent_option: User specified option for indentation
|
:param None|bool|int|str auto_indent_option:
|
||||||
from command line, config or extra kwarg. Accepts int, bool or str.
|
User specified option for indentation from command line, config
|
||||||
str option accepts the same range of values as boolean config options,
|
or extra kwarg. Accepts int, bool or str. str option accepts the
|
||||||
as well as positive integers represented in str form.
|
same range of values as boolean config options, as well as
|
||||||
|
positive integers represented in str form.
|
||||||
|
|
||||||
:returns: indentation value, which can be
|
:returns:
|
||||||
|
Indentation value, which can be
|
||||||
-1 (automatically determine indentation) or
|
-1 (automatically determine indentation) or
|
||||||
0 (auto-indent turned off) or
|
0 (auto-indent turned off) or
|
||||||
>0 (explicitly set indentation position).
|
>0 (explicitly set indentation position).
|
||||||
|
@ -164,7 +165,7 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||||
def format(self, record: logging.LogRecord) -> str:
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
if "\n" in record.message:
|
if "\n" in record.message:
|
||||||
if hasattr(record, "auto_indent"):
|
if hasattr(record, "auto_indent"):
|
||||||
# passed in from the "extra={}" kwarg on the call to logging.log()
|
# Passed in from the "extra={}" kwarg on the call to logging.log().
|
||||||
auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined]
|
auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined]
|
||||||
else:
|
else:
|
||||||
auto_indent = self._auto_indent
|
auto_indent = self._auto_indent
|
||||||
|
@ -178,7 +179,7 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||||
lines[0]
|
lines[0]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# optimizes logging by allowing a fixed indentation
|
# Optimizes logging by allowing a fixed indentation.
|
||||||
indentation = auto_indent
|
indentation = auto_indent
|
||||||
lines[0] = formatted
|
lines[0] = formatted
|
||||||
return ("\n" + " " * indentation).join(lines)
|
return ("\n" + " " * indentation).join(lines)
|
||||||
|
@ -316,7 +317,7 @@ class LogCaptureHandler(logging.StreamHandler):
|
||||||
stream = None # type: StringIO
|
stream = None # type: StringIO
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Creates a new log handler."""
|
"""Create a new log handler."""
|
||||||
super().__init__(StringIO())
|
super().__init__(StringIO())
|
||||||
self.records = [] # type: List[logging.LogRecord]
|
self.records = [] # type: List[logging.LogRecord]
|
||||||
|
|
||||||
|
@ -342,18 +343,17 @@ class LogCaptureFixture:
|
||||||
"""Provides access and control of log capturing."""
|
"""Provides access and control of log capturing."""
|
||||||
|
|
||||||
def __init__(self, item: nodes.Node) -> None:
|
def __init__(self, item: nodes.Node) -> None:
|
||||||
"""Creates a new funcarg."""
|
|
||||||
self._item = item
|
self._item = item
|
||||||
# dict of log name -> log level
|
|
||||||
self._initial_handler_level = None # type: Optional[int]
|
self._initial_handler_level = None # type: Optional[int]
|
||||||
|
# Dict of log name -> log level.
|
||||||
self._initial_logger_levels = {} # type: Dict[Optional[str], int]
|
self._initial_logger_levels = {} # type: Dict[Optional[str], int]
|
||||||
|
|
||||||
def _finalize(self) -> None:
|
def _finalize(self) -> None:
|
||||||
"""Finalizes the fixture.
|
"""Finalize the fixture.
|
||||||
|
|
||||||
This restores the log levels changed by :meth:`set_level`.
|
This restores the log levels changed by :meth:`set_level`.
|
||||||
"""
|
"""
|
||||||
# restore log levels
|
# Restore log levels.
|
||||||
if self._initial_handler_level is not None:
|
if self._initial_handler_level is not None:
|
||||||
self.handler.setLevel(self._initial_handler_level)
|
self.handler.setLevel(self._initial_handler_level)
|
||||||
for logger_name, level in self._initial_logger_levels.items():
|
for logger_name, level in self._initial_logger_levels.items():
|
||||||
|
@ -362,20 +362,20 @@ class LogCaptureFixture:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def handler(self) -> LogCaptureHandler:
|
def handler(self) -> LogCaptureHandler:
|
||||||
"""
|
"""Get the logging handler used by the fixture.
|
||||||
|
|
||||||
:rtype: LogCaptureHandler
|
:rtype: LogCaptureHandler
|
||||||
"""
|
"""
|
||||||
return self._item._store[caplog_handler_key]
|
return self._item._store[caplog_handler_key]
|
||||||
|
|
||||||
def get_records(self, when: str) -> List[logging.LogRecord]:
|
def get_records(self, when: str) -> List[logging.LogRecord]:
|
||||||
"""
|
"""Get the logging records for one of the possible test phases.
|
||||||
Get the logging records for one of the possible test phases.
|
|
||||||
|
|
||||||
:param str when:
|
:param str when:
|
||||||
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
|
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
|
||||||
|
|
||||||
|
:returns: The list of captured records at the given stage.
|
||||||
:rtype: List[logging.LogRecord]
|
:rtype: List[logging.LogRecord]
|
||||||
:return: the list of captured records at the given stage
|
|
||||||
|
|
||||||
.. versionadded:: 3.4
|
.. versionadded:: 3.4
|
||||||
"""
|
"""
|
||||||
|
@ -383,17 +383,17 @@ class LogCaptureFixture:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self) -> str:
|
def text(self) -> str:
|
||||||
"""Returns the formatted log text."""
|
"""The formatted log text."""
|
||||||
return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
|
return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def records(self) -> List[logging.LogRecord]:
|
def records(self) -> List[logging.LogRecord]:
|
||||||
"""Returns the list of log records."""
|
"""The list of log records."""
|
||||||
return self.handler.records
|
return self.handler.records
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def record_tuples(self) -> List[Tuple[str, int, str]]:
|
def record_tuples(self) -> List[Tuple[str, int, str]]:
|
||||||
"""Returns a list of a stripped down version of log records intended
|
"""A list of a stripped down version of log records intended
|
||||||
for use in assertion comparison.
|
for use in assertion comparison.
|
||||||
|
|
||||||
The format of the tuple is:
|
The format of the tuple is:
|
||||||
|
@ -404,15 +404,18 @@ class LogCaptureFixture:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def messages(self) -> List[str]:
|
def messages(self) -> List[str]:
|
||||||
"""Returns a list of format-interpolated log messages.
|
"""A list of format-interpolated log messages.
|
||||||
|
|
||||||
Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
|
Unlike 'records', which contains the format string and parameters for
|
||||||
are all interpolated.
|
interpolation, log messages in this list are all interpolated.
|
||||||
Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
|
|
||||||
levels, timestamps, etc, making exact comparisons more reliable.
|
|
||||||
|
|
||||||
Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
|
Unlike 'text', which contains the output from the handler, log
|
||||||
to the logging functions) is not included, as this is added by the formatter in the handler.
|
messages in this list are unadorned with levels, timestamps, etc,
|
||||||
|
making exact comparisons more reliable.
|
||||||
|
|
||||||
|
Note that traceback or stack info (from :func:`logging.exception` or
|
||||||
|
the `exc_info` or `stack_info` arguments to the logging functions) is
|
||||||
|
not included, as this is added by the formatter in the handler.
|
||||||
|
|
||||||
.. versionadded:: 3.7
|
.. versionadded:: 3.7
|
||||||
"""
|
"""
|
||||||
|
@ -423,18 +426,17 @@ class LogCaptureFixture:
|
||||||
self.handler.reset()
|
self.handler.reset()
|
||||||
|
|
||||||
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
||||||
"""Sets the level for capturing of logs. The level will be restored to its previous value at the end of
|
"""Set the level of a logger for the duration of a test.
|
||||||
the test.
|
|
||||||
|
|
||||||
:param int level: the logger to level.
|
|
||||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.4
|
.. versionchanged:: 3.4
|
||||||
The levels of the loggers changed by this function will be restored to their initial values at the
|
The levels of the loggers changed by this function will be
|
||||||
end of the test.
|
restored to their initial values at the end of the test.
|
||||||
|
|
||||||
|
:param int level: The level.
|
||||||
|
:param str logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
logger_obj = logging.getLogger(logger)
|
logger_obj = logging.getLogger(logger)
|
||||||
# save the original log-level to restore it during teardown
|
# Save the original log-level to restore it during teardown.
|
||||||
self._initial_logger_levels.setdefault(logger, logger_obj.level)
|
self._initial_logger_levels.setdefault(logger, logger_obj.level)
|
||||||
logger_obj.setLevel(level)
|
logger_obj.setLevel(level)
|
||||||
self._initial_handler_level = self.handler.level
|
self._initial_handler_level = self.handler.level
|
||||||
|
@ -444,11 +446,12 @@ class LogCaptureFixture:
|
||||||
def at_level(
|
def at_level(
|
||||||
self, level: int, logger: Optional[str] = None
|
self, level: int, logger: Optional[str] = None
|
||||||
) -> Generator[None, None, None]:
|
) -> Generator[None, None, None]:
|
||||||
"""Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
|
"""Context manager that sets the level for capturing of logs. After
|
||||||
level is restored to its original value.
|
the end of the 'with' statement the level is restored to its original
|
||||||
|
value.
|
||||||
|
|
||||||
:param int level: the logger to level.
|
:param int level: The level.
|
||||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
:param str logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
logger_obj = logging.getLogger(logger)
|
logger_obj = logging.getLogger(logger)
|
||||||
orig_level = logger_obj.level
|
orig_level = logger_obj.level
|
||||||
|
@ -509,11 +512,10 @@ def pytest_configure(config: Config) -> None:
|
||||||
|
|
||||||
|
|
||||||
class LoggingPlugin:
|
class LoggingPlugin:
|
||||||
"""Attaches to the logging module and captures log messages for each test.
|
"""Attaches to the logging module and captures log messages for each test."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, config: Config) -> None:
|
def __init__(self, config: Config) -> None:
|
||||||
"""Creates a new plugin to capture log messages.
|
"""Create a new plugin to capture log messages.
|
||||||
|
|
||||||
The formatter can be safely shared across all handlers so
|
The formatter can be safely shared across all handlers so
|
||||||
create a single one for the entire test session here.
|
create a single one for the entire test session here.
|
||||||
|
@ -572,7 +574,7 @@ class LoggingPlugin:
|
||||||
self.log_cli_handler.setFormatter(log_cli_formatter)
|
self.log_cli_handler.setFormatter(log_cli_formatter)
|
||||||
|
|
||||||
def _create_formatter(self, log_format, log_date_format, auto_indent):
|
def _create_formatter(self, log_format, log_date_format, auto_indent):
|
||||||
# color option doesn't exist if terminal plugin is disabled
|
# Color option doesn't exist if terminal plugin is disabled.
|
||||||
color = getattr(self._config.option, "color", "no")
|
color = getattr(self._config.option, "color", "no")
|
||||||
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
|
if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
|
||||||
log_format
|
log_format
|
||||||
|
@ -590,12 +592,12 @@ class LoggingPlugin:
|
||||||
return formatter
|
return formatter
|
||||||
|
|
||||||
def set_log_path(self, fname: str) -> None:
|
def set_log_path(self, fname: str) -> None:
|
||||||
"""Public method, which can set filename parameter for
|
"""Set the filename parameter for Logging.FileHandler().
|
||||||
Logging.FileHandler(). Also creates parent directory if
|
|
||||||
it does not exist.
|
Creates parent directory if it does not exist.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
Please considered as an experimental API.
|
This is an experimental API.
|
||||||
"""
|
"""
|
||||||
fpath = Path(fname)
|
fpath = Path(fname)
|
||||||
|
|
||||||
|
@ -652,19 +654,17 @@ class LoggingPlugin:
|
||||||
|
|
||||||
@pytest.hookimpl(hookwrapper=True)
|
@pytest.hookimpl(hookwrapper=True)
|
||||||
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
|
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]:
|
||||||
"""Runs all collected test items."""
|
|
||||||
|
|
||||||
if session.config.option.collectonly:
|
if session.config.option.collectonly:
|
||||||
yield
|
yield
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
|
if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
|
||||||
# setting verbose flag is needed to avoid messy test progress output
|
# The verbose flag is needed to avoid messy test progress output.
|
||||||
self._config.option.verbose = 1
|
self._config.option.verbose = 1
|
||||||
|
|
||||||
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
with catching_logs(self.log_cli_handler, level=self.log_cli_level):
|
||||||
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
with catching_logs(self.log_file_handler, level=self.log_file_level):
|
||||||
yield # run all the tests
|
yield # Run all the tests.
|
||||||
|
|
||||||
@pytest.hookimpl
|
@pytest.hookimpl
|
||||||
def pytest_runtest_logstart(self) -> None:
|
def pytest_runtest_logstart(self) -> None:
|
||||||
|
@ -676,7 +676,7 @@ class LoggingPlugin:
|
||||||
self.log_cli_handler.set_when("logreport")
|
self.log_cli_handler.set_when("logreport")
|
||||||
|
|
||||||
def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]:
|
def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]:
|
||||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
"""Implement the internals of the pytest_runtest_xxx() hooks."""
|
||||||
with catching_logs(
|
with catching_logs(
|
||||||
self.caplog_handler, level=self.log_level,
|
self.caplog_handler, level=self.log_level,
|
||||||
) as caplog_handler, catching_logs(
|
) as caplog_handler, catching_logs(
|
||||||
|
@ -734,9 +734,7 @@ class LoggingPlugin:
|
||||||
|
|
||||||
|
|
||||||
class _FileHandler(logging.FileHandler):
|
class _FileHandler(logging.FileHandler):
|
||||||
"""
|
"""A logging FileHandler with pytest tweaks."""
|
||||||
Custom FileHandler with pytest tweaks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def handleError(self, record: logging.LogRecord) -> None:
|
def handleError(self, record: logging.LogRecord) -> None:
|
||||||
# Handled by LogCaptureHandler.
|
# Handled by LogCaptureHandler.
|
||||||
|
@ -744,12 +742,12 @@ class _FileHandler(logging.FileHandler):
|
||||||
|
|
||||||
|
|
||||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||||
"""
|
"""A logging StreamHandler used by the live logging feature: it will
|
||||||
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
|
write a newline before the first log message in each test.
|
||||||
in each test.
|
|
||||||
|
|
||||||
During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
|
During live logging we must also explicitly disable stdout/stderr
|
||||||
and won't appear in the terminal.
|
capturing otherwise it will get captured and won't appear in the
|
||||||
|
terminal.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Officially stream needs to be a IO[str], but TerminalReporter
|
# Officially stream needs to be a IO[str], but TerminalReporter
|
||||||
|
@ -761,10 +759,6 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||||
terminal_reporter: TerminalReporter,
|
terminal_reporter: TerminalReporter,
|
||||||
capture_manager: Optional[CaptureManager],
|
capture_manager: Optional[CaptureManager],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
:param _pytest.terminal.TerminalReporter terminal_reporter:
|
|
||||||
:param _pytest.capture.CaptureManager capture_manager:
|
|
||||||
"""
|
|
||||||
logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type]
|
logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type]
|
||||||
self.capture_manager = capture_manager
|
self.capture_manager = capture_manager
|
||||||
self.reset()
|
self.reset()
|
||||||
|
@ -772,11 +766,11 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||||
self._test_outcome_written = False
|
self._test_outcome_written = False
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the handler; should be called before the start of each test"""
|
"""Reset the handler; should be called before the start of each test."""
|
||||||
self._first_record_emitted = False
|
self._first_record_emitted = False
|
||||||
|
|
||||||
def set_when(self, when: Optional[str]) -> None:
|
def set_when(self, when: Optional[str]) -> None:
|
||||||
"""Prepares for the given test phase (setup/call/teardown)"""
|
"""Prepare for the given test phase (setup/call/teardown)."""
|
||||||
self._when = when
|
self._when = when
|
||||||
self._section_name_shown = False
|
self._section_name_shown = False
|
||||||
if when == "start":
|
if when == "start":
|
||||||
|
@ -807,7 +801,7 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||||
|
|
||||||
|
|
||||||
class _LiveLoggingNullHandler(logging.NullHandler):
|
class _LiveLoggingNullHandler(logging.NullHandler):
|
||||||
"""A handler used when live logging is disabled."""
|
"""A logging handler used when live logging is disabled."""
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" core implementation of testing process: init, session, runtest loop. """
|
"""Core implementation of the testing process: init, session, runtest loop."""
|
||||||
import argparse
|
import argparse
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
|
@ -206,7 +206,7 @@ def validate_basetemp(path: str) -> str:
|
||||||
raise argparse.ArgumentTypeError(msg)
|
raise argparse.ArgumentTypeError(msg)
|
||||||
|
|
||||||
def is_ancestor(base: Path, query: Path) -> bool:
|
def is_ancestor(base: Path, query: Path) -> bool:
|
||||||
""" return True if query is an ancestor of base, else False."""
|
"""Return whether query is an ancestor of base."""
|
||||||
if base == query:
|
if base == query:
|
||||||
return True
|
return True
|
||||||
for parent in base.parents:
|
for parent in base.parents:
|
||||||
|
@ -228,7 +228,7 @@ def validate_basetemp(path: str) -> str:
|
||||||
def wrap_session(
|
def wrap_session(
|
||||||
config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
|
config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
|
||||||
) -> Union[int, ExitCode]:
|
) -> Union[int, ExitCode]:
|
||||||
"""Skeleton command line program"""
|
"""Skeleton command line program."""
|
||||||
session = Session.from_config(config)
|
session = Session.from_config(config)
|
||||||
session.exitstatus = ExitCode.OK
|
session.exitstatus = ExitCode.OK
|
||||||
initstate = 0
|
initstate = 0
|
||||||
|
@ -291,8 +291,8 @@ def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:
|
||||||
|
|
||||||
|
|
||||||
def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
|
def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
|
||||||
""" default command line protocol for initialization, session,
|
"""Default command line protocol for initialization, session,
|
||||||
running tests and reporting. """
|
running tests and reporting."""
|
||||||
config.hook.pytest_collection(session=session)
|
config.hook.pytest_collection(session=session)
|
||||||
config.hook.pytest_runtestloop(session=session)
|
config.hook.pytest_runtestloop(session=session)
|
||||||
|
|
||||||
|
@ -328,8 +328,8 @@ def pytest_runtestloop(session: "Session") -> bool:
|
||||||
|
|
||||||
|
|
||||||
def _in_venv(path: py.path.local) -> bool:
|
def _in_venv(path: py.path.local) -> bool:
|
||||||
"""Attempts to detect if ``path`` is the root of a Virtual Environment by
|
"""Attempt to detect if ``path`` is the root of a Virtual Environment by
|
||||||
checking for the existence of the appropriate activate script"""
|
checking for the existence of the appropriate activate script."""
|
||||||
bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin")
|
bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin")
|
||||||
if not bindir.isdir():
|
if not bindir.isdir():
|
||||||
return False
|
return False
|
||||||
|
@ -390,17 +390,17 @@ def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> No
|
||||||
|
|
||||||
|
|
||||||
class NoMatch(Exception):
|
class NoMatch(Exception):
|
||||||
""" raised if matching cannot locate a matching names. """
|
"""Matching cannot locate matching names."""
|
||||||
|
|
||||||
|
|
||||||
class Interrupted(KeyboardInterrupt):
|
class Interrupted(KeyboardInterrupt):
|
||||||
""" signals an interrupted test run. """
|
"""Signals that the test run was interrupted."""
|
||||||
|
|
||||||
__module__ = "builtins" # for py3
|
__module__ = "builtins" # For py3.
|
||||||
|
|
||||||
|
|
||||||
class Failed(Exception):
|
class Failed(Exception):
|
||||||
""" signals a stop as failed test run. """
|
"""Signals a stop as failed test run."""
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
|
@ -434,7 +434,7 @@ class Session(nodes.FSCollector):
|
||||||
self.startdir = config.invocation_dir
|
self.startdir = config.invocation_dir
|
||||||
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
|
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
|
||||||
|
|
||||||
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
# Keep track of any collected nodes in here, so we don't duplicate fixtures.
|
||||||
self._collection_node_cache1 = (
|
self._collection_node_cache1 = (
|
||||||
{}
|
{}
|
||||||
) # type: Dict[py.path.local, Sequence[nodes.Collector]]
|
) # type: Dict[py.path.local, Sequence[nodes.Collector]]
|
||||||
|
@ -469,7 +469,7 @@ class Session(nodes.FSCollector):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _node_location_to_relpath(self, node_path: py.path.local) -> str:
|
def _node_location_to_relpath(self, node_path: py.path.local) -> str:
|
||||||
# bestrelpath is a quite slow function
|
# bestrelpath is a quite slow function.
|
||||||
return self._bestrelpathcache[node_path]
|
return self._bestrelpathcache[node_path]
|
||||||
|
|
||||||
@hookimpl(tryfirst=True)
|
@hookimpl(tryfirst=True)
|
||||||
|
@ -594,7 +594,7 @@ class Session(nodes.FSCollector):
|
||||||
|
|
||||||
# Start with a Session root, and delve to argpath item (dir or file)
|
# Start with a Session root, and delve to argpath item (dir or file)
|
||||||
# and stack all Packages found on the way.
|
# and stack all Packages found on the way.
|
||||||
# No point in finding packages when collecting doctests
|
# No point in finding packages when collecting doctests.
|
||||||
if not self.config.getoption("doctestmodules", False):
|
if not self.config.getoption("doctestmodules", False):
|
||||||
pm = self.config.pluginmanager
|
pm = self.config.pluginmanager
|
||||||
for parent in reversed(argpath.parts()):
|
for parent in reversed(argpath.parts()):
|
||||||
|
@ -609,7 +609,7 @@ class Session(nodes.FSCollector):
|
||||||
if col:
|
if col:
|
||||||
if isinstance(col[0], Package):
|
if isinstance(col[0], Package):
|
||||||
self._collection_pkg_roots[str(parent)] = col[0]
|
self._collection_pkg_roots[str(parent)] = col[0]
|
||||||
# always store a list in the cache, matchnodes expects it
|
# Always store a list in the cache, matchnodes expects it.
|
||||||
self._collection_node_cache1[col[0].fspath] = [col[0]]
|
self._collection_node_cache1[col[0].fspath] = [col[0]]
|
||||||
|
|
||||||
# If it's a directory argument, recurse and look for any Subpackages.
|
# If it's a directory argument, recurse and look for any Subpackages.
|
||||||
|
@ -689,7 +689,7 @@ class Session(nodes.FSCollector):
|
||||||
return spec.origin
|
return spec.origin
|
||||||
|
|
||||||
def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]:
|
def _parsearg(self, arg: str) -> Tuple[py.path.local, List[str]]:
|
||||||
""" return (fspath, names) tuple after checking the file exists. """
|
"""Return (fspath, names) tuple after checking the file exists."""
|
||||||
strpath, *parts = str(arg).split("::")
|
strpath, *parts = str(arg).split("::")
|
||||||
if self.config.option.pyargs:
|
if self.config.option.pyargs:
|
||||||
strpath = self._tryconvertpyarg(strpath)
|
strpath = self._tryconvertpyarg(strpath)
|
||||||
|
@ -740,18 +740,18 @@ class Session(nodes.FSCollector):
|
||||||
if rep.passed:
|
if rep.passed:
|
||||||
has_matched = False
|
has_matched = False
|
||||||
for x in rep.result:
|
for x in rep.result:
|
||||||
# TODO: remove parametrized workaround once collection structure contains parametrization
|
# TODO: Remove parametrized workaround once collection structure contains parametrization.
|
||||||
if x.name == name or x.name.split("[")[0] == name:
|
if x.name == name or x.name.split("[")[0] == name:
|
||||||
resultnodes.extend(self.matchnodes([x], nextnames))
|
resultnodes.extend(self.matchnodes([x], nextnames))
|
||||||
has_matched = True
|
has_matched = True
|
||||||
# XXX accept IDs that don't have "()" for class instances
|
# XXX Accept IDs that don't have "()" for class instances.
|
||||||
if not has_matched and len(rep.result) == 1 and x.name == "()":
|
if not has_matched and len(rep.result) == 1 and x.name == "()":
|
||||||
nextnames.insert(0, name)
|
nextnames.insert(0, name)
|
||||||
resultnodes.extend(self.matchnodes([x], nextnames))
|
resultnodes.extend(self.matchnodes([x], nextnames))
|
||||||
else:
|
else:
|
||||||
# report collection failures here to avoid failing to run some test
|
# Report collection failures here to avoid failing to run some test
|
||||||
# specified in the command line because the module could not be
|
# specified in the command line because the module could not be
|
||||||
# imported (#134)
|
# imported (#134).
|
||||||
node.ihook.pytest_collectreport(report=rep)
|
node.ihook.pytest_collectreport(report=rep)
|
||||||
return resultnodes
|
return resultnodes
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" generic mechanism for marking and selecting python functions. """
|
"""Generic mechanism for marking and selecting python functions."""
|
||||||
import typing
|
import typing
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -58,9 +58,9 @@ def param(
|
||||||
def test_eval(test_input, expected):
|
def test_eval(test_input, expected):
|
||||||
assert eval(test_input) == expected
|
assert eval(test_input) == expected
|
||||||
|
|
||||||
:param values: variable args of the values of the parameter set, in order.
|
:param values: Variable args of the values of the parameter set, in order.
|
||||||
:keyword marks: a single mark or a list of marks to be applied to this parameter set.
|
:keyword marks: A single mark or a list of marks to be applied to this parameter set.
|
||||||
:keyword str id: the id to attribute to this parameter set.
|
:keyword str id: The id to attribute to this parameter set.
|
||||||
"""
|
"""
|
||||||
return ParameterSet.param(*values, marks=marks, id=id)
|
return ParameterSet.param(*values, marks=marks, id=id)
|
||||||
|
|
||||||
|
@ -148,22 +148,22 @@ class KeywordMatcher:
|
||||||
def from_item(cls, item: "Item") -> "KeywordMatcher":
|
def from_item(cls, item: "Item") -> "KeywordMatcher":
|
||||||
mapped_names = set()
|
mapped_names = set()
|
||||||
|
|
||||||
# Add the names of the current item and any parent items
|
# Add the names of the current item and any parent items.
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
for node in item.listchain():
|
for node in item.listchain():
|
||||||
if not isinstance(node, (pytest.Instance, pytest.Session)):
|
if not isinstance(node, (pytest.Instance, pytest.Session)):
|
||||||
mapped_names.add(node.name)
|
mapped_names.add(node.name)
|
||||||
|
|
||||||
# Add the names added as extra keywords to current or parent items
|
# Add the names added as extra keywords to current or parent items.
|
||||||
mapped_names.update(item.listextrakeywords())
|
mapped_names.update(item.listextrakeywords())
|
||||||
|
|
||||||
# Add the names attached to the current function through direct assignment
|
# Add the names attached to the current function through direct assignment.
|
||||||
function_obj = getattr(item, "function", None)
|
function_obj = getattr(item, "function", None)
|
||||||
if function_obj:
|
if function_obj:
|
||||||
mapped_names.update(function_obj.__dict__)
|
mapped_names.update(function_obj.__dict__)
|
||||||
|
|
||||||
# add the markers to the keywords as we no longer handle them correctly
|
# Add the markers to the keywords as we no longer handle them correctly.
|
||||||
mapped_names.update(mark.name for mark in item.iter_markers())
|
mapped_names.update(mark.name for mark in item.iter_markers())
|
||||||
|
|
||||||
return cls(mapped_names)
|
return cls(mapped_names)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
r"""
|
r"""Evaluate match expressions, as used by `-k` and `-m`.
|
||||||
Evaluate match expressions, as used by `-k` and `-m`.
|
|
||||||
|
|
||||||
The grammar is:
|
The grammar is:
|
||||||
|
|
||||||
|
@ -213,10 +212,11 @@ class Expression:
|
||||||
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
|
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
|
||||||
"""Evaluate the match expression.
|
"""Evaluate the match expression.
|
||||||
|
|
||||||
:param matcher: Given an identifier, should return whether it matches or not.
|
:param matcher:
|
||||||
Should be prepared to handle arbitrary strings as input.
|
Given an identifier, should return whether it matches or not.
|
||||||
|
Should be prepared to handle arbitrary strings as input.
|
||||||
|
|
||||||
Returns whether the expression matches or not.
|
:returns: Whether the expression matches or not.
|
||||||
"""
|
"""
|
||||||
ret = eval(
|
ret = eval(
|
||||||
self.code, {"__builtins__": {}}, MatcherAdapter(matcher)
|
self.code, {"__builtins__": {}}, MatcherAdapter(matcher)
|
||||||
|
|
|
@ -107,14 +107,15 @@ class ParameterSet(
|
||||||
parameterset: Union["ParameterSet", Sequence[object], object],
|
parameterset: Union["ParameterSet", Sequence[object], object],
|
||||||
force_tuple: bool = False,
|
force_tuple: bool = False,
|
||||||
) -> "ParameterSet":
|
) -> "ParameterSet":
|
||||||
"""
|
"""Extract from an object or objects.
|
||||||
|
|
||||||
:param parameterset:
|
:param parameterset:
|
||||||
a legacy style parameterset that may or may not be a tuple,
|
A legacy style parameterset that may or may not be a tuple,
|
||||||
and may or may not be wrapped into a mess of mark objects
|
and may or may not be wrapped into a mess of mark objects.
|
||||||
|
|
||||||
:param force_tuple:
|
:param force_tuple:
|
||||||
enforce tuple wrapping so single argument tuple values
|
Enforce tuple wrapping so single argument tuple values
|
||||||
don't get decomposed and break tests
|
don't get decomposed and break tests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(parameterset, cls):
|
if isinstance(parameterset, cls):
|
||||||
|
@ -166,7 +167,7 @@ class ParameterSet(
|
||||||
del argvalues
|
del argvalues
|
||||||
|
|
||||||
if parameters:
|
if parameters:
|
||||||
# check all parameter sets have the correct number of values
|
# Check all parameter sets have the correct number of values.
|
||||||
for param in parameters:
|
for param in parameters:
|
||||||
if len(param.values) != len(argnames):
|
if len(param.values) != len(argnames):
|
||||||
msg = (
|
msg = (
|
||||||
|
@ -186,8 +187,8 @@ class ParameterSet(
|
||||||
pytrace=False,
|
pytrace=False,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# empty parameter set (likely computed at runtime): create a single
|
# Empty parameter set (likely computed at runtime): create a single
|
||||||
# parameter set with NOTSET values, with the "empty parameter set" mark applied to it
|
# parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
|
||||||
mark = get_empty_parameterset_mark(config, argnames, func)
|
mark = get_empty_parameterset_mark(config, argnames, func)
|
||||||
parameters.append(
|
parameters.append(
|
||||||
ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
|
ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
|
||||||
|
@ -220,8 +221,7 @@ class Mark:
|
||||||
|
|
||||||
Combines by appending args and merging kwargs.
|
Combines by appending args and merging kwargs.
|
||||||
|
|
||||||
:param other: The mark to combine with.
|
:param Mark other: The mark to combine with.
|
||||||
:type other: Mark
|
|
||||||
:rtype: Mark
|
:rtype: Mark
|
||||||
"""
|
"""
|
||||||
assert self.name == other.name
|
assert self.name == other.name
|
||||||
|
@ -314,7 +314,7 @@ class MarkDecorator:
|
||||||
Unlike calling the MarkDecorator, with_args() can be used even
|
Unlike calling the MarkDecorator, with_args() can be used even
|
||||||
if the sole argument is a callable/class.
|
if the sole argument is a callable/class.
|
||||||
|
|
||||||
:return: MarkDecorator
|
:rtype: MarkDecorator
|
||||||
"""
|
"""
|
||||||
mark = Mark(self.name, args, kwargs)
|
mark = Mark(self.name, args, kwargs)
|
||||||
return self.__class__(self.mark.combined_with(mark))
|
return self.__class__(self.mark.combined_with(mark))
|
||||||
|
@ -344,9 +344,7 @@ class MarkDecorator:
|
||||||
|
|
||||||
|
|
||||||
def get_unpacked_marks(obj) -> List[Mark]:
|
def get_unpacked_marks(obj) -> List[Mark]:
|
||||||
"""
|
"""Obtain the unpacked marks that are stored on an object."""
|
||||||
obtain the unpacked marks that are stored on an object
|
|
||||||
"""
|
|
||||||
mark_list = getattr(obj, "pytestmark", [])
|
mark_list = getattr(obj, "pytestmark", [])
|
||||||
if not isinstance(mark_list, list):
|
if not isinstance(mark_list, list):
|
||||||
mark_list = [mark_list]
|
mark_list = [mark_list]
|
||||||
|
@ -354,10 +352,9 @@ def get_unpacked_marks(obj) -> List[Mark]:
|
||||||
|
|
||||||
|
|
||||||
def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
|
def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
|
||||||
"""
|
"""Normalize marker decorating helpers to mark objects.
|
||||||
normalizes marker decorating helpers to mark objects
|
|
||||||
|
|
||||||
:type mark_list: List[Union[Mark, Markdecorator]]
|
:type List[Union[Mark, Markdecorator]] mark_list:
|
||||||
:rtype: List[Mark]
|
:rtype: List[Mark]
|
||||||
"""
|
"""
|
||||||
extracted = [
|
extracted = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" monkeypatching and mocking functionality. """
|
"""Monkeypatching and mocking functionality."""
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
@ -27,8 +27,10 @@ V = TypeVar("V")
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
def monkeypatch() -> Generator["MonkeyPatch", None, None]:
|
def monkeypatch() -> Generator["MonkeyPatch", None, None]:
|
||||||
"""The returned ``monkeypatch`` fixture provides these
|
"""A convenient fixture for monkey-patching.
|
||||||
helper methods to modify objects, dictionaries or os.environ::
|
|
||||||
|
The fixture provides these methods to modify objects, dictionaries or
|
||||||
|
os.environ::
|
||||||
|
|
||||||
monkeypatch.setattr(obj, name, value, raising=True)
|
monkeypatch.setattr(obj, name, value, raising=True)
|
||||||
monkeypatch.delattr(obj, name, raising=True)
|
monkeypatch.delattr(obj, name, raising=True)
|
||||||
|
@ -39,10 +41,9 @@ def monkeypatch() -> Generator["MonkeyPatch", None, None]:
|
||||||
monkeypatch.syspath_prepend(path)
|
monkeypatch.syspath_prepend(path)
|
||||||
monkeypatch.chdir(path)
|
monkeypatch.chdir(path)
|
||||||
|
|
||||||
All modifications will be undone after the requesting
|
All modifications will be undone after the requesting test function or
|
||||||
test function or fixture has finished. The ``raising``
|
fixture has finished. The ``raising`` parameter determines if a KeyError
|
||||||
parameter determines if a KeyError or AttributeError
|
or AttributeError will be raised if the set/deletion operation has no target.
|
||||||
will be raised if the set/deletion operation has no target.
|
|
||||||
"""
|
"""
|
||||||
mpatch = MonkeyPatch()
|
mpatch = MonkeyPatch()
|
||||||
yield mpatch
|
yield mpatch
|
||||||
|
@ -50,7 +51,7 @@ def monkeypatch() -> Generator["MonkeyPatch", None, None]:
|
||||||
|
|
||||||
|
|
||||||
def resolve(name: str) -> object:
|
def resolve(name: str) -> object:
|
||||||
# simplified from zope.dottedname
|
# Simplified from zope.dottedname.
|
||||||
parts = name.split(".")
|
parts = name.split(".")
|
||||||
|
|
||||||
used = parts.pop(0)
|
used = parts.pop(0)
|
||||||
|
@ -63,12 +64,11 @@ def resolve(name: str) -> object:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
# we use explicit un-nesting of the handling block in order
|
# We use explicit un-nesting of the handling block in order
|
||||||
# to avoid nested exceptions on python 3
|
# to avoid nested exceptions.
|
||||||
try:
|
try:
|
||||||
__import__(used)
|
__import__(used)
|
||||||
except ImportError as ex:
|
except ImportError as ex:
|
||||||
# str is used for py2 vs py3
|
|
||||||
expected = str(ex).split()[-1]
|
expected = str(ex).split()[-1]
|
||||||
if expected == used:
|
if expected == used:
|
||||||
raise
|
raise
|
||||||
|
@ -111,8 +111,8 @@ notset = Notset()
|
||||||
|
|
||||||
|
|
||||||
class MonkeyPatch:
|
class MonkeyPatch:
|
||||||
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
|
"""Object returned by the ``monkeypatch`` fixture keeping a record of
|
||||||
"""
|
setattr/item/env/syspath changes."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._setattr = [] # type: List[Tuple[object, str, object]]
|
self._setattr = [] # type: List[Tuple[object, str, object]]
|
||||||
|
@ -124,9 +124,10 @@ class MonkeyPatch:
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def context(self) -> Generator["MonkeyPatch", None, None]:
|
def context(self) -> Generator["MonkeyPatch", None, None]:
|
||||||
"""
|
"""Context manager that returns a new :class:`MonkeyPatch` object
|
||||||
Context manager that returns a new :class:`MonkeyPatch` object which
|
which undoes any patching done inside the ``with`` block upon exit.
|
||||||
undoes any patching done inside the ``with`` block upon exit:
|
|
||||||
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -166,18 +167,16 @@ class MonkeyPatch:
|
||||||
value: object = notset,
|
value: object = notset,
|
||||||
raising: bool = True,
|
raising: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
""" Set attribute value on target, memorizing the old value.
|
"""Set attribute value on target, memorizing the old value.
|
||||||
By default raise AttributeError if the attribute did not exist.
|
|
||||||
|
|
||||||
For convenience you can specify a string as ``target`` which
|
For convenience you can specify a string as ``target`` which
|
||||||
will be interpreted as a dotted import path, with the last part
|
will be interpreted as a dotted import path, with the last part
|
||||||
being the attribute name. Example:
|
being the attribute name. For example,
|
||||||
``monkeypatch.setattr("os.getcwd", lambda: "/")``
|
``monkeypatch.setattr("os.getcwd", lambda: "/")``
|
||||||
would set the ``getcwd`` function of the ``os`` module.
|
would set the ``getcwd`` function of the ``os`` module.
|
||||||
|
|
||||||
The ``raising`` value determines if the setattr should fail
|
Raises AttributeError if the attribute does not exist, unless
|
||||||
if the attribute is not already present (defaults to True
|
``raising`` is set to False.
|
||||||
which means it will raise).
|
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -215,15 +214,14 @@ class MonkeyPatch:
|
||||||
name: Union[str, Notset] = notset,
|
name: Union[str, Notset] = notset,
|
||||||
raising: bool = True,
|
raising: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
""" Delete attribute ``name`` from ``target``, by default raise
|
"""Delete attribute ``name`` from ``target``.
|
||||||
AttributeError it the attribute did not previously exist.
|
|
||||||
|
|
||||||
If no ``name`` is specified and ``target`` is a string
|
If no ``name`` is specified and ``target`` is a string
|
||||||
it will be interpreted as a dotted import path with the
|
it will be interpreted as a dotted import path with the
|
||||||
last part being the attribute name.
|
last part being the attribute name.
|
||||||
|
|
||||||
If ``raising`` is set to False, no exception will be raised if the
|
Raises AttributeError it the attribute does not exist, unless
|
||||||
attribute is missing.
|
``raising`` is set to False.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -249,15 +247,15 @@ class MonkeyPatch:
|
||||||
delattr(target, name)
|
delattr(target, name)
|
||||||
|
|
||||||
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
||||||
""" Set dictionary entry ``name`` to value. """
|
"""Set dictionary entry ``name`` to value."""
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
dic[name] = value
|
dic[name] = value
|
||||||
|
|
||||||
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
||||||
""" Delete ``name`` from dict. Raise KeyError if it doesn't exist.
|
"""Delete ``name`` from dict.
|
||||||
|
|
||||||
If ``raising`` is set to False, no exception will be raised if the
|
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
||||||
key is missing.
|
False.
|
||||||
"""
|
"""
|
||||||
if name not in dic:
|
if name not in dic:
|
||||||
if raising:
|
if raising:
|
||||||
|
@ -267,9 +265,12 @@ class MonkeyPatch:
|
||||||
del dic[name]
|
del dic[name]
|
||||||
|
|
||||||
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
||||||
""" Set environment variable ``name`` to ``value``. If ``prepend``
|
"""Set environment variable ``name`` to ``value``.
|
||||||
is a character, read the current environment variable value
|
|
||||||
and prepend the ``value`` adjoined with the ``prepend`` character."""
|
If ``prepend`` is a character, read the current environment variable
|
||||||
|
value and prepend the ``value`` adjoined with the ``prepend``
|
||||||
|
character.
|
||||||
|
"""
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
pytest.PytestWarning(
|
pytest.PytestWarning(
|
||||||
|
@ -286,17 +287,16 @@ class MonkeyPatch:
|
||||||
self.setitem(os.environ, name, value)
|
self.setitem(os.environ, name, value)
|
||||||
|
|
||||||
def delenv(self, name: str, raising: bool = True) -> None:
|
def delenv(self, name: str, raising: bool = True) -> None:
|
||||||
""" Delete ``name`` from the environment. Raise KeyError if it does
|
"""Delete ``name`` from the environment.
|
||||||
not exist.
|
|
||||||
|
|
||||||
If ``raising`` is set to False, no exception will be raised if the
|
Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
|
||||||
environment variable is missing.
|
False.
|
||||||
"""
|
"""
|
||||||
environ = os.environ # type: MutableMapping[str, str]
|
environ = os.environ # type: MutableMapping[str, str]
|
||||||
self.delitem(environ, name, raising=raising)
|
self.delitem(environ, name, raising=raising)
|
||||||
|
|
||||||
def syspath_prepend(self, path) -> None:
|
def syspath_prepend(self, path) -> None:
|
||||||
""" Prepend ``path`` to ``sys.path`` list of import locations. """
|
"""Prepend ``path`` to ``sys.path`` list of import locations."""
|
||||||
from pkg_resources import fixup_namespace_packages
|
from pkg_resources import fixup_namespace_packages
|
||||||
|
|
||||||
if self._savesyspath is None:
|
if self._savesyspath is None:
|
||||||
|
@ -318,7 +318,8 @@ class MonkeyPatch:
|
||||||
invalidate_caches()
|
invalidate_caches()
|
||||||
|
|
||||||
def chdir(self, path) -> None:
|
def chdir(self, path) -> None:
|
||||||
""" Change the current working directory to the specified path.
|
"""Change the current working directory to the specified path.
|
||||||
|
|
||||||
Path can be a string or a py.path.local object.
|
Path can be a string or a py.path.local object.
|
||||||
"""
|
"""
|
||||||
if self._cwd is None:
|
if self._cwd is None:
|
||||||
|
@ -326,15 +327,16 @@ class MonkeyPatch:
|
||||||
if hasattr(path, "chdir"):
|
if hasattr(path, "chdir"):
|
||||||
path.chdir()
|
path.chdir()
|
||||||
elif isinstance(path, Path):
|
elif isinstance(path, Path):
|
||||||
# modern python uses the fspath protocol here LEGACY
|
# Modern python uses the fspath protocol here LEGACY
|
||||||
os.chdir(str(path))
|
os.chdir(str(path))
|
||||||
else:
|
else:
|
||||||
os.chdir(path)
|
os.chdir(path)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
""" Undo previous changes. This call consumes the
|
"""Undo previous changes.
|
||||||
undo stack. Calling it a second time has no effect unless
|
|
||||||
you do more monkeypatching after the undo call.
|
This call consumes the undo stack. Calling it a second time has no
|
||||||
|
effect unless you do more monkeypatching after the undo call.
|
||||||
|
|
||||||
There is generally no need to call `undo()`, since it is
|
There is generally no need to call `undo()`, since it is
|
||||||
called automatically during tear-down.
|
called automatically during tear-down.
|
||||||
|
@ -356,7 +358,7 @@ class MonkeyPatch:
|
||||||
try:
|
try:
|
||||||
del dictionary[key]
|
del dictionary[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass # was already deleted, so we have the desired state
|
pass # Was already deleted, so we have the desired state.
|
||||||
else:
|
else:
|
||||||
dictionary[key] = value
|
dictionary[key] = value
|
||||||
self._setitem[:] = []
|
self._setitem[:] = []
|
||||||
|
|
|
@ -66,19 +66,23 @@ def _splitnode(nodeid: str) -> Tuple[str, ...]:
|
||||||
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
|
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo']
|
||||||
"""
|
"""
|
||||||
if nodeid == "":
|
if nodeid == "":
|
||||||
# If there is no root node at all, return an empty list so the caller's logic can remain sane
|
# If there is no root node at all, return an empty list so the caller's
|
||||||
|
# logic can remain sane.
|
||||||
return ()
|
return ()
|
||||||
parts = nodeid.split(SEP)
|
parts = nodeid.split(SEP)
|
||||||
# Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar'
|
# Replace single last element 'test_foo.py::Bar' with multiple elements
|
||||||
|
# 'test_foo.py', 'Bar'.
|
||||||
parts[-1:] = parts[-1].split("::")
|
parts[-1:] = parts[-1].split("::")
|
||||||
# Convert parts into a tuple to avoid possible errors with caching of a mutable type
|
# Convert parts into a tuple to avoid possible errors with caching of a
|
||||||
|
# mutable type.
|
||||||
return tuple(parts)
|
return tuple(parts)
|
||||||
|
|
||||||
|
|
||||||
def ischildnode(baseid: str, nodeid: str) -> bool:
|
def ischildnode(baseid: str, nodeid: str) -> bool:
|
||||||
"""Return True if the nodeid is a child node of the baseid.
|
"""Return True if the nodeid is a child node of the baseid.
|
||||||
|
|
||||||
E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
|
E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz',
|
||||||
|
but not of 'foo/blorp'.
|
||||||
"""
|
"""
|
||||||
base_parts = _splitnode(baseid)
|
base_parts = _splitnode(baseid)
|
||||||
node_parts = _splitnode(nodeid)
|
node_parts = _splitnode(nodeid)
|
||||||
|
@ -100,8 +104,11 @@ class NodeMeta(type):
|
||||||
|
|
||||||
|
|
||||||
class Node(metaclass=NodeMeta):
|
class Node(metaclass=NodeMeta):
|
||||||
""" base class for Collector and Item the test collection tree.
|
"""Base class for Collector and Item, the components of the test
|
||||||
Collector subclasses have children, Items are terminal nodes."""
|
collection tree.
|
||||||
|
|
||||||
|
Collector subclasses have children; Items are leaf nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
# Use __slots__ to make attribute access faster.
|
# Use __slots__ to make attribute access faster.
|
||||||
# Note that __dict__ is still available.
|
# Note that __dict__ is still available.
|
||||||
|
@ -125,13 +132,13 @@ class Node(metaclass=NodeMeta):
|
||||||
fspath: Optional[py.path.local] = None,
|
fspath: Optional[py.path.local] = None,
|
||||||
nodeid: Optional[str] = None,
|
nodeid: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
#: a unique name within the scope of the parent node
|
#: A unique name within the scope of the parent node.
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
#: the parent collector node.
|
#: The parent collector node.
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
#: the pytest config object
|
#: The pytest config object.
|
||||||
if config:
|
if config:
|
||||||
self.config = config # type: Config
|
self.config = config # type: Config
|
||||||
else:
|
else:
|
||||||
|
@ -139,7 +146,7 @@ class Node(metaclass=NodeMeta):
|
||||||
raise TypeError("config or parent must be provided")
|
raise TypeError("config or parent must be provided")
|
||||||
self.config = parent.config
|
self.config = parent.config
|
||||||
|
|
||||||
#: the session this node is part of
|
#: The pytest session this node is part of.
|
||||||
if session:
|
if session:
|
||||||
self.session = session
|
self.session = session
|
||||||
else:
|
else:
|
||||||
|
@ -147,19 +154,19 @@ class Node(metaclass=NodeMeta):
|
||||||
raise TypeError("session or parent must be provided")
|
raise TypeError("session or parent must be provided")
|
||||||
self.session = parent.session
|
self.session = parent.session
|
||||||
|
|
||||||
#: filesystem path where this node was collected from (can be None)
|
#: Filesystem path where this node was collected from (can be None).
|
||||||
self.fspath = fspath or getattr(parent, "fspath", None)
|
self.fspath = fspath or getattr(parent, "fspath", None)
|
||||||
|
|
||||||
#: keywords/markers collected from all scopes
|
#: Keywords/markers collected from all scopes.
|
||||||
self.keywords = NodeKeywords(self)
|
self.keywords = NodeKeywords(self)
|
||||||
|
|
||||||
#: the marker objects belonging to this node
|
#: The marker objects belonging to this node.
|
||||||
self.own_markers = [] # type: List[Mark]
|
self.own_markers = [] # type: List[Mark]
|
||||||
|
|
||||||
#: allow adding of extra keywords to use for matching
|
#: Allow adding of extra keywords to use for matching.
|
||||||
self.extra_keyword_matches = set() # type: Set[str]
|
self.extra_keyword_matches = set() # type: Set[str]
|
||||||
|
|
||||||
# used for storing artificial fixturedefs for direct parametrization
|
# Used for storing artificial fixturedefs for direct parametrization.
|
||||||
self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef]
|
self._name2pseudofixturedef = {} # type: Dict[str, FixtureDef]
|
||||||
|
|
||||||
if nodeid is not None:
|
if nodeid is not None:
|
||||||
|
@ -178,15 +185,15 @@ class Node(metaclass=NodeMeta):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent: "Node", **kw):
|
def from_parent(cls, parent: "Node", **kw):
|
||||||
"""
|
"""Public constructor for Nodes.
|
||||||
Public Constructor for Nodes
|
|
||||||
|
|
||||||
This indirection got introduced in order to enable removing
|
This indirection got introduced in order to enable removing
|
||||||
the fragile logic from the node constructors.
|
the fragile logic from the node constructors.
|
||||||
|
|
||||||
Subclasses can use ``super().from_parent(...)`` when overriding the construction
|
Subclasses can use ``super().from_parent(...)`` when overriding the
|
||||||
|
construction.
|
||||||
|
|
||||||
:param parent: the parent node of this test Node
|
:param parent: The parent node of this Node.
|
||||||
"""
|
"""
|
||||||
if "config" in kw:
|
if "config" in kw:
|
||||||
raise TypeError("config is not a valid argument for from_parent")
|
raise TypeError("config is not a valid argument for from_parent")
|
||||||
|
@ -196,27 +203,27 @@ class Node(metaclass=NodeMeta):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ihook(self):
|
def ihook(self):
|
||||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
"""fspath-sensitive hook proxy used to call pytest hooks."""
|
||||||
return self.session.gethookproxy(self.fspath)
|
return self.session.gethookproxy(self.fspath)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
|
return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
|
||||||
|
|
||||||
def warn(self, warning: "PytestWarning") -> None:
|
def warn(self, warning: "PytestWarning") -> None:
|
||||||
"""Issue a warning for this item.
|
"""Issue a warning for this Node.
|
||||||
|
|
||||||
Warnings will be displayed after the test session, unless explicitly suppressed
|
Warnings will be displayed after the test session, unless explicitly suppressed.
|
||||||
|
|
||||||
:param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
|
:param Warning warning:
|
||||||
|
The warning instance to issue. Must be a subclass of PytestWarning.
|
||||||
|
|
||||||
:raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
|
:raises ValueError: If ``warning`` instance is not a subclass of PytestWarning.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
node.warn(PytestWarning("some message"))
|
node.warn(PytestWarning("some message"))
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
@ -232,10 +239,11 @@ class Node(metaclass=NodeMeta):
|
||||||
warning, category=None, filename=str(path), lineno=lineno + 1,
|
warning, category=None, filename=str(path), lineno=lineno + 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
# methods for ordering nodes
|
# Methods for ordering nodes.
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nodeid(self) -> str:
|
def nodeid(self) -> str:
|
||||||
""" a ::-separated string denoting its collection tree address. """
|
"""A ::-separated string denoting its collection tree address."""
|
||||||
return self._nodeid
|
return self._nodeid
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
|
@ -248,8 +256,8 @@ class Node(metaclass=NodeMeta):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def listchain(self) -> List["Node"]:
|
def listchain(self) -> List["Node"]:
|
||||||
""" return list of all parent collectors up to self,
|
"""Return list of all parent collectors up to self, starting from
|
||||||
starting from root of collection tree. """
|
the root of collection tree."""
|
||||||
chain = []
|
chain = []
|
||||||
item = self # type: Optional[Node]
|
item = self # type: Optional[Node]
|
||||||
while item is not None:
|
while item is not None:
|
||||||
|
@ -261,12 +269,10 @@ class Node(metaclass=NodeMeta):
|
||||||
def add_marker(
|
def add_marker(
|
||||||
self, marker: Union[str, MarkDecorator], append: bool = True
|
self, marker: Union[str, MarkDecorator], append: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
"""dynamically add a marker object to the node.
|
"""Dynamically add a marker object to the node.
|
||||||
|
|
||||||
:type marker: ``str`` or ``pytest.mark.*`` object
|
:param append:
|
||||||
:param marker:
|
Whether to append the marker, or prepend it.
|
||||||
``append=True`` whether to append the marker,
|
|
||||||
if ``False`` insert at position ``0``.
|
|
||||||
"""
|
"""
|
||||||
from _pytest.mark import MARK_GEN
|
from _pytest.mark import MARK_GEN
|
||||||
|
|
||||||
|
@ -283,21 +289,19 @@ class Node(metaclass=NodeMeta):
|
||||||
self.own_markers.insert(0, marker_.mark)
|
self.own_markers.insert(0, marker_.mark)
|
||||||
|
|
||||||
def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
|
def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
|
||||||
"""
|
"""Iterate over all markers of the node.
|
||||||
:param name: if given, filter the results by the name attribute
|
|
||||||
|
|
||||||
iterate over all markers of the node
|
:param name: If given, filter the results by the name attribute.
|
||||||
"""
|
"""
|
||||||
return (x[1] for x in self.iter_markers_with_node(name=name))
|
return (x[1] for x in self.iter_markers_with_node(name=name))
|
||||||
|
|
||||||
def iter_markers_with_node(
|
def iter_markers_with_node(
|
||||||
self, name: Optional[str] = None
|
self, name: Optional[str] = None
|
||||||
) -> Iterator[Tuple["Node", Mark]]:
|
) -> Iterator[Tuple["Node", Mark]]:
|
||||||
"""
|
"""Iterate over all markers of the node.
|
||||||
:param name: if given, filter the results by the name attribute
|
|
||||||
|
|
||||||
iterate over all markers of the node
|
:param name: If given, filter the results by the name attribute.
|
||||||
returns sequence of tuples (node, mark)
|
:returns: An iterator of (node, mark) tuples.
|
||||||
"""
|
"""
|
||||||
for node in reversed(self.listchain()):
|
for node in reversed(self.listchain()):
|
||||||
for mark in node.own_markers:
|
for mark in node.own_markers:
|
||||||
|
@ -315,16 +319,16 @@ class Node(metaclass=NodeMeta):
|
||||||
def get_closest_marker( # noqa: F811
|
def get_closest_marker( # noqa: F811
|
||||||
self, name: str, default: Optional[Mark] = None
|
self, name: str, default: Optional[Mark] = None
|
||||||
) -> Optional[Mark]:
|
) -> Optional[Mark]:
|
||||||
"""return the first marker matching the name, from closest (for example function) to farther level (for example
|
"""Return the first marker matching the name, from closest (for
|
||||||
module level).
|
example function) to farther level (for example module level).
|
||||||
|
|
||||||
:param default: fallback return value of no marker was found
|
:param default: Fallback return value if no marker was found.
|
||||||
:param name: name to filter by
|
:param name: Name to filter by.
|
||||||
"""
|
"""
|
||||||
return next(self.iter_markers(name=name), default)
|
return next(self.iter_markers(name=name), default)
|
||||||
|
|
||||||
def listextrakeywords(self) -> Set[str]:
|
def listextrakeywords(self) -> Set[str]:
|
||||||
""" Return a set of all extra keywords in self and any parents."""
|
"""Return a set of all extra keywords in self and any parents."""
|
||||||
extra_keywords = set() # type: Set[str]
|
extra_keywords = set() # type: Set[str]
|
||||||
for item in self.listchain():
|
for item in self.listchain():
|
||||||
extra_keywords.update(item.extra_keyword_matches)
|
extra_keywords.update(item.extra_keyword_matches)
|
||||||
|
@ -334,7 +338,7 @@ class Node(metaclass=NodeMeta):
|
||||||
return [x.name for x in self.listchain()]
|
return [x.name for x in self.listchain()]
|
||||||
|
|
||||||
def addfinalizer(self, fin: Callable[[], object]) -> None:
|
def addfinalizer(self, fin: Callable[[], object]) -> None:
|
||||||
""" register a function to be called when this node is finalized.
|
"""Register a function to be called when this node is finalized.
|
||||||
|
|
||||||
This method can only be called when this node is active
|
This method can only be called when this node is active
|
||||||
in a setup chain, for example during self.setup().
|
in a setup chain, for example during self.setup().
|
||||||
|
@ -342,8 +346,8 @@ class Node(metaclass=NodeMeta):
|
||||||
self.session._setupstate.addfinalizer(fin, self)
|
self.session._setupstate.addfinalizer(fin, self)
|
||||||
|
|
||||||
def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]:
|
def getparent(self, cls: "Type[_NodeType]") -> Optional[_NodeType]:
|
||||||
""" get the next parent node (including ourself)
|
"""Get the next parent node (including self) which is an instance of
|
||||||
which is an instance of the given class"""
|
the given class."""
|
||||||
current = self # type: Optional[Node]
|
current = self # type: Optional[Node]
|
||||||
while current and not isinstance(current, cls):
|
while current and not isinstance(current, cls):
|
||||||
current = current.parent
|
current = current.parent
|
||||||
|
@ -411,8 +415,7 @@ class Node(metaclass=NodeMeta):
|
||||||
excinfo: ExceptionInfo[BaseException],
|
excinfo: ExceptionInfo[BaseException],
|
||||||
style: "Optional[_TracebackStyle]" = None,
|
style: "Optional[_TracebackStyle]" = None,
|
||||||
) -> Union[str, TerminalRepr]:
|
) -> Union[str, TerminalRepr]:
|
||||||
"""
|
"""Return a representation of a collection or test failure.
|
||||||
Return a representation of a collection or test failure.
|
|
||||||
|
|
||||||
:param excinfo: Exception information for the failure.
|
:param excinfo: Exception information for the failure.
|
||||||
"""
|
"""
|
||||||
|
@ -422,13 +425,13 @@ class Node(metaclass=NodeMeta):
|
||||||
def get_fslocation_from_item(
|
def get_fslocation_from_item(
|
||||||
node: "Node",
|
node: "Node",
|
||||||
) -> Tuple[Union[str, py.path.local], Optional[int]]:
|
) -> Tuple[Union[str, py.path.local], Optional[int]]:
|
||||||
"""Tries to extract the actual location from a node, depending on available attributes:
|
"""Try to extract the actual location from a node, depending on available attributes:
|
||||||
|
|
||||||
* "location": a pair (path, lineno)
|
* "location": a pair (path, lineno)
|
||||||
* "obj": a Python object that the node wraps.
|
* "obj": a Python object that the node wraps.
|
||||||
* "fspath": just a path
|
* "fspath": just a path
|
||||||
|
|
||||||
:rtype: a tuple of (str|LocalPath, int) with filename and line number.
|
:rtype: A tuple of (str|py.path.local, int) with filename and line number.
|
||||||
"""
|
"""
|
||||||
# See Item.location.
|
# See Item.location.
|
||||||
location = getattr(
|
location = getattr(
|
||||||
|
@ -443,25 +446,22 @@ def get_fslocation_from_item(
|
||||||
|
|
||||||
|
|
||||||
class Collector(Node):
|
class Collector(Node):
|
||||||
""" Collector instances create children through collect()
|
"""Collector instances create children through collect() and thus
|
||||||
and thus iteratively build a tree.
|
iteratively build a tree."""
|
||||||
"""
|
|
||||||
|
|
||||||
class CollectError(Exception):
|
class CollectError(Exception):
|
||||||
""" an error during collection, contains a custom message. """
|
"""An error during collection, contains a custom message."""
|
||||||
|
|
||||||
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
def collect(self) -> Iterable[Union["Item", "Collector"]]:
|
||||||
""" returns a list of children (items and collectors)
|
"""Return a list of children (items and collectors) for this
|
||||||
for this collection node.
|
collection node."""
|
||||||
"""
|
|
||||||
raise NotImplementedError("abstract")
|
raise NotImplementedError("abstract")
|
||||||
|
|
||||||
# TODO: This omits the style= parameter which breaks Liskov Substitution.
|
# TODO: This omits the style= parameter which breaks Liskov Substitution.
|
||||||
def repr_failure( # type: ignore[override]
|
def repr_failure( # type: ignore[override]
|
||||||
self, excinfo: ExceptionInfo[BaseException]
|
self, excinfo: ExceptionInfo[BaseException]
|
||||||
) -> Union[str, TerminalRepr]:
|
) -> Union[str, TerminalRepr]:
|
||||||
"""
|
"""Return a representation of a collection failure.
|
||||||
Return a representation of a collection failure.
|
|
||||||
|
|
||||||
:param excinfo: Exception information for the failure.
|
:param excinfo: Exception information for the failure.
|
||||||
"""
|
"""
|
||||||
|
@ -538,24 +538,22 @@ class FSCollector(Collector):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent, *, fspath, **kw):
|
def from_parent(cls, parent, *, fspath, **kw):
|
||||||
"""
|
"""The public constructor."""
|
||||||
The public constructor
|
|
||||||
"""
|
|
||||||
return super().from_parent(parent=parent, fspath=fspath, **kw)
|
return super().from_parent(parent=parent, fspath=fspath, **kw)
|
||||||
|
|
||||||
def _gethookproxy(self, fspath: py.path.local):
|
def _gethookproxy(self, fspath: py.path.local):
|
||||||
# check if we have the common case of running
|
# Check if we have the common case of running
|
||||||
# hooks with all conftest.py files
|
# hooks with all conftest.py files.
|
||||||
pm = self.config.pluginmanager
|
pm = self.config.pluginmanager
|
||||||
my_conftestmodules = pm._getconftestmodules(
|
my_conftestmodules = pm._getconftestmodules(
|
||||||
fspath, self.config.getoption("importmode")
|
fspath, self.config.getoption("importmode")
|
||||||
)
|
)
|
||||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||||
if remove_mods:
|
if remove_mods:
|
||||||
# one or more conftests are not in use at this fspath
|
# One or more conftests are not in use at this fspath.
|
||||||
proxy = FSHookProxy(pm, remove_mods)
|
proxy = FSHookProxy(pm, remove_mods)
|
||||||
else:
|
else:
|
||||||
# all plugins are active for this fspath
|
# All plugins are active for this fspath.
|
||||||
proxy = self.config.hook
|
proxy = self.config.hook
|
||||||
return proxy
|
return proxy
|
||||||
|
|
||||||
|
@ -605,12 +603,13 @@ class FSCollector(Collector):
|
||||||
|
|
||||||
|
|
||||||
class File(FSCollector):
|
class File(FSCollector):
|
||||||
""" base class for collecting tests from a file. """
|
"""Base class for collecting tests from a file."""
|
||||||
|
|
||||||
|
|
||||||
class Item(Node):
|
class Item(Node):
|
||||||
""" a basic test invocation item. Note that for a single function
|
"""A basic test invocation item.
|
||||||
there might be multiple test invocation items.
|
|
||||||
|
Note that for a single function there might be multiple test invocation items.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
nextitem = None
|
nextitem = None
|
||||||
|
@ -626,17 +625,16 @@ class Item(Node):
|
||||||
super().__init__(name, parent, config, session, nodeid=nodeid)
|
super().__init__(name, parent, config, session, nodeid=nodeid)
|
||||||
self._report_sections = [] # type: List[Tuple[str, str, str]]
|
self._report_sections = [] # type: List[Tuple[str, str, str]]
|
||||||
|
|
||||||
#: user properties is a list of tuples (name, value) that holds user
|
#: A list of tuples (name, value) that holds user defined properties
|
||||||
#: defined properties for this test.
|
#: for this test.
|
||||||
self.user_properties = [] # type: List[Tuple[str, object]]
|
self.user_properties = [] # type: List[Tuple[str, object]]
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
raise NotImplementedError("runtest must be implemented by Item subclass")
|
raise NotImplementedError("runtest must be implemented by Item subclass")
|
||||||
|
|
||||||
def add_report_section(self, when: str, key: str, content: str) -> None:
|
def add_report_section(self, when: str, key: str, content: str) -> None:
|
||||||
"""
|
"""Add a new report section, similar to what's done internally to add
|
||||||
Adds a new report section, similar to what's done internally to add stdout and
|
stdout and stderr captured output::
|
||||||
stderr captured output::
|
|
||||||
|
|
||||||
item.add_report_section("call", "stdout", "report section contents")
|
item.add_report_section("call", "stdout", "report section contents")
|
||||||
|
|
||||||
|
@ -645,7 +643,6 @@ class Item(Node):
|
||||||
:param str key:
|
:param str key:
|
||||||
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
|
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
|
||||||
``"stderr"`` internally.
|
``"stderr"`` internally.
|
||||||
|
|
||||||
:param str content:
|
:param str content:
|
||||||
The full contents as a string.
|
The full contents as a string.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" run test suites written for nose. """
|
"""Run testsuites written for nose."""
|
||||||
from _pytest import python
|
from _pytest import python
|
||||||
from _pytest import unittest
|
from _pytest import unittest
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
|
@ -9,9 +9,9 @@ from _pytest.nodes import Item
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
if is_potential_nosetest(item):
|
if is_potential_nosetest(item):
|
||||||
if not call_optional(item.obj, "setup"):
|
if not call_optional(item.obj, "setup"):
|
||||||
# call module level setup if there is no object level one
|
# Call module level setup if there is no object level one.
|
||||||
call_optional(item.parent.obj, "setup")
|
call_optional(item.parent.obj, "setup")
|
||||||
# XXX this implies we only call teardown when setup worked
|
# XXX This implies we only call teardown when setup worked.
|
||||||
item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
|
item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,8 +22,8 @@ def teardown_nose(item):
|
||||||
|
|
||||||
|
|
||||||
def is_potential_nosetest(item: Item) -> bool:
|
def is_potential_nosetest(item: Item) -> bool:
|
||||||
# extra check needed since we do not do nose style setup/teardown
|
# Extra check needed since we do not do nose style setup/teardown
|
||||||
# on direct unittest style classes
|
# on direct unittest style classes.
|
||||||
return isinstance(item, python.Function) and not isinstance(
|
return isinstance(item, python.Function) and not isinstance(
|
||||||
item, unittest.TestCaseFunction
|
item, unittest.TestCaseFunction
|
||||||
)
|
)
|
||||||
|
@ -34,6 +34,6 @@ def call_optional(obj, name):
|
||||||
isfixture = hasattr(method, "_pytestfixturefunction")
|
isfixture = hasattr(method, "_pytestfixturefunction")
|
||||||
if method is not None and not isfixture and callable(method):
|
if method is not None and not isfixture and callable(method):
|
||||||
# If there's any problems allow the exception to raise rather than
|
# If there's any problems allow the exception to raise rather than
|
||||||
# silently ignoring them
|
# silently ignoring them.
|
||||||
method()
|
method()
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
"""
|
"""Exception classes and constants handling test outcomes as well as
|
||||||
exception classes and constants handling test outcomes
|
functions creating them."""
|
||||||
as well as functions creating them
|
|
||||||
"""
|
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
@ -9,7 +7,7 @@ from typing import cast
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
TYPE_CHECKING = False # avoid circular import through compat
|
TYPE_CHECKING = False # Avoid circular import through compat.
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
@ -25,9 +23,8 @@ else:
|
||||||
|
|
||||||
|
|
||||||
class OutcomeException(BaseException):
|
class OutcomeException(BaseException):
|
||||||
""" OutcomeException and its subclass instances indicate and
|
"""OutcomeException and its subclass instances indicate and contain info
|
||||||
contain info about test and collection outcomes.
|
about test and collection outcomes."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
|
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
|
||||||
if msg is not None and not isinstance(msg, str):
|
if msg is not None and not isinstance(msg, str):
|
||||||
|
@ -67,13 +64,13 @@ class Skipped(OutcomeException):
|
||||||
|
|
||||||
|
|
||||||
class Failed(OutcomeException):
|
class Failed(OutcomeException):
|
||||||
""" raised from an explicit call to pytest.fail() """
|
"""Raised from an explicit call to pytest.fail()."""
|
||||||
|
|
||||||
__module__ = "builtins"
|
__module__ = "builtins"
|
||||||
|
|
||||||
|
|
||||||
class Exit(Exception):
|
class Exit(Exception):
|
||||||
""" raised for immediate program exits (no tracebacks/summaries)"""
|
"""Raised for immediate program exits (no tracebacks/summaries)."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, msg: str = "unknown reason", returncode: Optional[int] = None
|
self, msg: str = "unknown reason", returncode: Optional[int] = None
|
||||||
|
@ -104,16 +101,15 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E
|
||||||
return decorate
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
# exposed helper methods
|
# Exposed helper methods.
|
||||||
|
|
||||||
|
|
||||||
@_with_exception(Exit)
|
@_with_exception(Exit)
|
||||||
def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
|
def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
|
||||||
"""
|
"""Exit testing process.
|
||||||
Exit testing process.
|
|
||||||
|
|
||||||
:param str msg: message to display upon exit.
|
:param str msg: Message to display upon exit.
|
||||||
:param int returncode: return code to be used when exiting pytest.
|
:param int returncode: Return code to be used when exiting pytest.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise Exit(msg, returncode)
|
raise Exit(msg, returncode)
|
||||||
|
@ -121,20 +117,20 @@ def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn":
|
||||||
|
|
||||||
@_with_exception(Skipped)
|
@_with_exception(Skipped)
|
||||||
def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
|
def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
|
||||||
"""
|
"""Skip an executing test with the given message.
|
||||||
Skip an executing test with the given message.
|
|
||||||
|
|
||||||
This function should be called only during testing (setup, call or teardown) or
|
This function should be called only during testing (setup, call or teardown) or
|
||||||
during collection by using the ``allow_module_level`` flag. This function can
|
during collection by using the ``allow_module_level`` flag. This function can
|
||||||
be called in doctests as well.
|
be called in doctests as well.
|
||||||
|
|
||||||
:kwarg bool allow_module_level: allows this function to be called at
|
:param bool allow_module_level:
|
||||||
module level, skipping the rest of the module. Default to False.
|
Allows this function to be called at module level, skipping the rest
|
||||||
|
of the module. Defaults to False.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be
|
It is better to use the :ref:`pytest.mark.skipif ref` marker when
|
||||||
skipped under certain conditions like mismatching platforms or
|
possible to declare a test to be skipped under certain conditions
|
||||||
dependencies.
|
like mismatching platforms or dependencies.
|
||||||
Similarly, use the ``# doctest: +SKIP`` directive (see `doctest.SKIP
|
Similarly, use the ``# doctest: +SKIP`` directive (see `doctest.SKIP
|
||||||
<https://docs.python.org/3/library/doctest.html#doctest.SKIP>`_)
|
<https://docs.python.org/3/library/doctest.html#doctest.SKIP>`_)
|
||||||
to skip a doctest statically.
|
to skip a doctest statically.
|
||||||
|
@ -145,11 +141,12 @@ def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn":
|
||||||
|
|
||||||
@_with_exception(Failed)
|
@_with_exception(Failed)
|
||||||
def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
|
def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
|
||||||
"""
|
"""Explicitly fail an executing test with the given message.
|
||||||
Explicitly fail an executing test with the given message.
|
|
||||||
|
|
||||||
:param str msg: the message to show the user as reason for the failure.
|
:param str msg:
|
||||||
:param bool pytrace: if false the msg represents the full failure information and no
|
The message to show the user as reason for the failure.
|
||||||
|
:param bool pytrace:
|
||||||
|
If False, msg represents the full failure information and no
|
||||||
python traceback will be reported.
|
python traceback will be reported.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
@ -157,19 +154,19 @@ def fail(msg: str = "", pytrace: bool = True) -> "NoReturn":
|
||||||
|
|
||||||
|
|
||||||
class XFailed(Failed):
|
class XFailed(Failed):
|
||||||
""" raised from an explicit call to pytest.xfail() """
|
"""Raised from an explicit call to pytest.xfail()."""
|
||||||
|
|
||||||
|
|
||||||
@_with_exception(XFailed)
|
@_with_exception(XFailed)
|
||||||
def xfail(reason: str = "") -> "NoReturn":
|
def xfail(reason: str = "") -> "NoReturn":
|
||||||
"""
|
"""Imperatively xfail an executing test or setup function with the given reason.
|
||||||
Imperatively xfail an executing test or setup functions with the given reason.
|
|
||||||
|
|
||||||
This function should be called only during testing (setup, call or teardown).
|
This function should be called only during testing (setup, call or teardown).
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be
|
It is better to use the :ref:`pytest.mark.xfail ref` marker when
|
||||||
xfailed under certain conditions like known bugs or missing features.
|
possible to declare a test to be xfailed under certain conditions
|
||||||
|
like known bugs or missing features.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
raise XFailed(reason)
|
raise XFailed(reason)
|
||||||
|
@ -178,17 +175,20 @@ def xfail(reason: str = "") -> "NoReturn":
|
||||||
def importorskip(
|
def importorskip(
|
||||||
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
|
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Imports and returns the requested module ``modname``, or skip the
|
"""Import and return the requested module ``modname``, or skip the
|
||||||
current test if the module cannot be imported.
|
current test if the module cannot be imported.
|
||||||
|
|
||||||
:param str modname: the name of the module to import
|
:param str modname:
|
||||||
:param str minversion: if given, the imported module's ``__version__``
|
The name of the module to import.
|
||||||
attribute must be at least this minimal version, otherwise the test is
|
:param str minversion:
|
||||||
still skipped.
|
If given, the imported module's ``__version__`` attribute must be at
|
||||||
:param str reason: if given, this reason is shown as the message when the
|
least this minimal version, otherwise the test is still skipped.
|
||||||
module cannot be imported.
|
:param str reason:
|
||||||
:returns: The imported module. This should be assigned to its canonical
|
If given, this reason is shown as the message when the module cannot
|
||||||
name.
|
be imported.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
The imported module. This should be assigned to its canonical name.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
|
@ -200,9 +200,9 @@ def importorskip(
|
||||||
compile(modname, "", "eval") # to catch syntaxerrors
|
compile(modname, "", "eval") # to catch syntaxerrors
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
# make sure to ignore ImportWarnings that might happen because
|
# Make sure to ignore ImportWarnings that might happen because
|
||||||
# of existing directories with the same name we're trying to
|
# of existing directories with the same name we're trying to
|
||||||
# import but without a __init__.py file
|
# import but without a __init__.py file.
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
try:
|
try:
|
||||||
__import__(modname)
|
__import__(modname)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" submit failure or test session information to a pastebin service. """
|
"""Submit failure or test session information to a pastebin service."""
|
||||||
import tempfile
|
import tempfile
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
@ -32,11 +32,11 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
if config.option.pastebin == "all":
|
if config.option.pastebin == "all":
|
||||||
tr = config.pluginmanager.getplugin("terminalreporter")
|
tr = config.pluginmanager.getplugin("terminalreporter")
|
||||||
# if no terminal reporter plugin is present, nothing we can do here;
|
# If no terminal reporter plugin is present, nothing we can do here;
|
||||||
# this can happen when this function executes in a worker node
|
# this can happen when this function executes in a worker node
|
||||||
# when using pytest-xdist, for example
|
# when using pytest-xdist, for example.
|
||||||
if tr is not None:
|
if tr is not None:
|
||||||
# pastebin file will be utf-8 encoded binary file
|
# pastebin file will be UTF-8 encoded binary file.
|
||||||
config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b")
|
config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b")
|
||||||
oldwrite = tr._tw.write
|
oldwrite = tr._tw.write
|
||||||
|
|
||||||
|
@ -52,26 +52,25 @@ def pytest_configure(config: Config) -> None:
|
||||||
def pytest_unconfigure(config: Config) -> None:
|
def pytest_unconfigure(config: Config) -> None:
|
||||||
if pastebinfile_key in config._store:
|
if pastebinfile_key in config._store:
|
||||||
pastebinfile = config._store[pastebinfile_key]
|
pastebinfile = config._store[pastebinfile_key]
|
||||||
# get terminal contents and delete file
|
# Get terminal contents and delete file.
|
||||||
pastebinfile.seek(0)
|
pastebinfile.seek(0)
|
||||||
sessionlog = pastebinfile.read()
|
sessionlog = pastebinfile.read()
|
||||||
pastebinfile.close()
|
pastebinfile.close()
|
||||||
del config._store[pastebinfile_key]
|
del config._store[pastebinfile_key]
|
||||||
# undo our patching in the terminal reporter
|
# Undo our patching in the terminal reporter.
|
||||||
tr = config.pluginmanager.getplugin("terminalreporter")
|
tr = config.pluginmanager.getplugin("terminalreporter")
|
||||||
del tr._tw.__dict__["write"]
|
del tr._tw.__dict__["write"]
|
||||||
# write summary
|
# Write summary.
|
||||||
tr.write_sep("=", "Sending information to Paste Service")
|
tr.write_sep("=", "Sending information to Paste Service")
|
||||||
pastebinurl = create_new_paste(sessionlog)
|
pastebinurl = create_new_paste(sessionlog)
|
||||||
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
|
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
|
||||||
|
|
||||||
|
|
||||||
def create_new_paste(contents: Union[str, bytes]) -> str:
|
def create_new_paste(contents: Union[str, bytes]) -> str:
|
||||||
"""
|
"""Create a new paste using the bpaste.net service.
|
||||||
Creates a new paste using bpaste.net service.
|
|
||||||
|
|
||||||
:contents: paste contents string
|
:contents: Paste contents string.
|
||||||
:returns: url to the pasted contents or error message
|
:returns: URL to the pasted contents, or an error message.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
|
@ -49,23 +49,21 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||||
|
|
||||||
|
|
||||||
def ensure_reset_dir(path: Path) -> None:
|
def ensure_reset_dir(path: Path) -> None:
|
||||||
"""
|
"""Ensure the given path is an empty directory."""
|
||||||
ensures the given path is an empty directory
|
|
||||||
"""
|
|
||||||
if path.exists():
|
if path.exists():
|
||||||
rm_rf(path)
|
rm_rf(path)
|
||||||
path.mkdir()
|
path.mkdir()
|
||||||
|
|
||||||
|
|
||||||
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||||
"""Handles known read-only errors during rmtree.
|
"""Handle known read-only errors during rmtree.
|
||||||
|
|
||||||
The returned value is used only by our own tests.
|
The returned value is used only by our own tests.
|
||||||
"""
|
"""
|
||||||
exctype, excvalue = exc[:2]
|
exctype, excvalue = exc[:2]
|
||||||
|
|
||||||
# another process removed the file in the middle of the "rm_rf" (xdist for example)
|
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
||||||
# more context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||||
if isinstance(excvalue, FileNotFoundError):
|
if isinstance(excvalue, FileNotFoundError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -101,7 +99,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||||
if p.is_file():
|
if p.is_file():
|
||||||
for parent in p.parents:
|
for parent in p.parents:
|
||||||
chmod_rw(str(parent))
|
chmod_rw(str(parent))
|
||||||
# stop when we reach the original path passed to rm_rf
|
# Stop when we reach the original path passed to rm_rf.
|
||||||
if parent == start_path:
|
if parent == start_path:
|
||||||
break
|
break
|
||||||
chmod_rw(str(path))
|
chmod_rw(str(path))
|
||||||
|
@ -129,7 +127,7 @@ def ensure_extended_length_path(path: Path) -> Path:
|
||||||
|
|
||||||
|
|
||||||
def get_extended_length_path_str(path: str) -> str:
|
def get_extended_length_path_str(path: str) -> str:
|
||||||
"""Converts to extended length path as a str"""
|
"""Convert a path to a Windows extended length path."""
|
||||||
long_path_prefix = "\\\\?\\"
|
long_path_prefix = "\\\\?\\"
|
||||||
unc_long_path_prefix = "\\\\?\\UNC\\"
|
unc_long_path_prefix = "\\\\?\\UNC\\"
|
||||||
if path.startswith((long_path_prefix, unc_long_path_prefix)):
|
if path.startswith((long_path_prefix, unc_long_path_prefix)):
|
||||||
|
@ -142,15 +140,14 @@ def get_extended_length_path_str(path: str) -> str:
|
||||||
|
|
||||||
def rm_rf(path: Path) -> None:
|
def rm_rf(path: Path) -> None:
|
||||||
"""Remove the path contents recursively, even if some elements
|
"""Remove the path contents recursively, even if some elements
|
||||||
are read-only.
|
are read-only."""
|
||||||
"""
|
|
||||||
path = ensure_extended_length_path(path)
|
path = ensure_extended_length_path(path)
|
||||||
onerror = partial(on_rm_rf_error, start_path=path)
|
onerror = partial(on_rm_rf_error, start_path=path)
|
||||||
shutil.rmtree(str(path), onerror=onerror)
|
shutil.rmtree(str(path), onerror=onerror)
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||||
"""finds all elements in root that begin with the prefix, case insensitive"""
|
"""Find all elements in root that begin with the prefix, case insensitive."""
|
||||||
l_prefix = prefix.lower()
|
l_prefix = prefix.lower()
|
||||||
for x in root.iterdir():
|
for x in root.iterdir():
|
||||||
if x.name.lower().startswith(l_prefix):
|
if x.name.lower().startswith(l_prefix):
|
||||||
|
@ -158,10 +155,10 @@ def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||||
|
|
||||||
|
|
||||||
def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
|
def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
|
||||||
"""
|
"""Return the parts of the paths following the prefix.
|
||||||
:param iter: iterator over path names
|
|
||||||
:param prefix: expected prefix of the path names
|
:param iter: Iterator over path names.
|
||||||
:returns: the parts of the paths following the prefix
|
:param prefix: Expected prefix of the path names.
|
||||||
"""
|
"""
|
||||||
p_len = len(prefix)
|
p_len = len(prefix)
|
||||||
for p in iter:
|
for p in iter:
|
||||||
|
@ -169,13 +166,12 @@ def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
|
||||||
|
|
||||||
|
|
||||||
def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
|
def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
|
||||||
"""combines find_prefixes and extract_suffixes
|
"""Combine find_prefixes and extract_suffixes."""
|
||||||
"""
|
|
||||||
return extract_suffixes(find_prefixed(root, prefix), prefix)
|
return extract_suffixes(find_prefixed(root, prefix), prefix)
|
||||||
|
|
||||||
|
|
||||||
def parse_num(maybe_num) -> int:
|
def parse_num(maybe_num) -> int:
|
||||||
"""parses number path suffixes, returns -1 on error"""
|
"""Parse number path suffixes, returns -1 on error."""
|
||||||
try:
|
try:
|
||||||
return int(maybe_num)
|
return int(maybe_num)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -185,13 +181,13 @@ def parse_num(maybe_num) -> int:
|
||||||
def _force_symlink(
|
def _force_symlink(
|
||||||
root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
|
root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""helper to create the current symlink
|
"""Helper to create the current symlink.
|
||||||
|
|
||||||
it's full of race conditions that are reasonably ok to ignore
|
It's full of race conditions that are reasonably OK to ignore
|
||||||
for the context of best effort linking to the latest test run
|
for the context of best effort linking to the latest test run.
|
||||||
|
|
||||||
the presumption being that in case of much parallelism
|
The presumption being that in case of much parallelism
|
||||||
the inaccuracy is going to be acceptable
|
the inaccuracy is going to be acceptable.
|
||||||
"""
|
"""
|
||||||
current_symlink = root.joinpath(target)
|
current_symlink = root.joinpath(target)
|
||||||
try:
|
try:
|
||||||
|
@ -205,7 +201,7 @@ def _force_symlink(
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir(root: Path, prefix: str) -> Path:
|
def make_numbered_dir(root: Path, prefix: str) -> Path:
|
||||||
"""create a directory with an increased number as suffix for the given prefix"""
|
"""Create a directory with an increased number as suffix for the given prefix."""
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
# try up to 10 times to create the folder
|
# try up to 10 times to create the folder
|
||||||
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||||
|
@ -226,7 +222,7 @@ def make_numbered_dir(root: Path, prefix: str) -> Path:
|
||||||
|
|
||||||
|
|
||||||
def create_cleanup_lock(p: Path) -> Path:
|
def create_cleanup_lock(p: Path) -> Path:
|
||||||
"""crates a lock to prevent premature folder cleanup"""
|
"""Create a lock to prevent premature folder cleanup."""
|
||||||
lock_path = get_lock_path(p)
|
lock_path = get_lock_path(p)
|
||||||
try:
|
try:
|
||||||
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
|
fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
|
||||||
|
@ -243,7 +239,7 @@ def create_cleanup_lock(p: Path) -> Path:
|
||||||
|
|
||||||
|
|
||||||
def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
|
def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
|
||||||
"""registers a cleanup function for removing a lock, by default on atexit"""
|
"""Register a cleanup function for removing a lock, by default on atexit."""
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
|
|
||||||
def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
|
def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
|
||||||
|
@ -260,7 +256,8 @@ def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
|
||||||
|
|
||||||
|
|
||||||
def maybe_delete_a_numbered_dir(path: Path) -> None:
|
def maybe_delete_a_numbered_dir(path: Path) -> None:
|
||||||
"""removes a numbered directory if its lock can be obtained and it does not seem to be in use"""
|
"""Remove a numbered directory if its lock can be obtained and it does
|
||||||
|
not seem to be in use."""
|
||||||
path = ensure_extended_length_path(path)
|
path = ensure_extended_length_path(path)
|
||||||
lock_path = None
|
lock_path = None
|
||||||
try:
|
try:
|
||||||
|
@ -277,8 +274,8 @@ def maybe_delete_a_numbered_dir(path: Path) -> None:
|
||||||
# * process cwd (Windows)
|
# * process cwd (Windows)
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
# if we created the lock, ensure we remove it even if we failed
|
# If we created the lock, ensure we remove it even if we failed
|
||||||
# to properly remove the numbered dir
|
# to properly remove the numbered dir.
|
||||||
if lock_path is not None:
|
if lock_path is not None:
|
||||||
try:
|
try:
|
||||||
lock_path.unlink()
|
lock_path.unlink()
|
||||||
|
@ -287,7 +284,7 @@ def maybe_delete_a_numbered_dir(path: Path) -> None:
|
||||||
|
|
||||||
|
|
||||||
def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
|
def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
|
||||||
"""checks if `path` is deletable based on whether the lock file is expired"""
|
"""Check if `path` is deletable based on whether the lock file is expired."""
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
return False
|
return False
|
||||||
lock = get_lock_path(path)
|
lock = get_lock_path(path)
|
||||||
|
@ -304,9 +301,9 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) ->
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if lock_time < consider_lock_dead_if_created_before:
|
if lock_time < consider_lock_dead_if_created_before:
|
||||||
# wa want to ignore any errors while trying to remove the lock such as:
|
# We want to ignore any errors while trying to remove the lock such as:
|
||||||
# - PermissionDenied, like the file permissions have changed since the lock creation
|
# - PermissionDenied, like the file permissions have changed since the lock creation;
|
||||||
# - FileNotFoundError, in case another pytest process got here first.
|
# - FileNotFoundError, in case another pytest process got here first;
|
||||||
# and any other cause of failure.
|
# and any other cause of failure.
|
||||||
with contextlib.suppress(OSError):
|
with contextlib.suppress(OSError):
|
||||||
lock.unlink()
|
lock.unlink()
|
||||||
|
@ -315,13 +312,13 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) ->
|
||||||
|
|
||||||
|
|
||||||
def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
|
def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
|
||||||
"""tries to cleanup a folder if we can ensure it's deletable"""
|
"""Try to cleanup a folder if we can ensure it's deletable."""
|
||||||
if ensure_deletable(path, consider_lock_dead_if_created_before):
|
if ensure_deletable(path, consider_lock_dead_if_created_before):
|
||||||
maybe_delete_a_numbered_dir(path)
|
maybe_delete_a_numbered_dir(path)
|
||||||
|
|
||||||
|
|
||||||
def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||||
"""lists candidates for numbered directories to be removed - follows py.path"""
|
"""List candidates for numbered directories to be removed - follows py.path."""
|
||||||
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||||
max_delete = max_existing - keep
|
max_delete = max_existing - keep
|
||||||
paths = find_prefixed(root, prefix)
|
paths = find_prefixed(root, prefix)
|
||||||
|
@ -335,7 +332,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||||
def cleanup_numbered_dir(
|
def cleanup_numbered_dir(
|
||||||
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
|
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
|
||||||
) -> None:
|
) -> None:
|
||||||
"""cleanup for lock driven numbered directories"""
|
"""Cleanup for lock driven numbered directories."""
|
||||||
for path in cleanup_candidates(root, prefix, keep):
|
for path in cleanup_candidates(root, prefix, keep):
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
for path in root.glob("garbage-*"):
|
for path in root.glob("garbage-*"):
|
||||||
|
@ -345,7 +342,7 @@ def cleanup_numbered_dir(
|
||||||
def make_numbered_dir_with_cleanup(
|
def make_numbered_dir_with_cleanup(
|
||||||
root: Path, prefix: str, keep: int, lock_timeout: float
|
root: Path, prefix: str, keep: int, lock_timeout: float
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""creates a numbered dir with a cleanup lock and removes old ones"""
|
"""Create a numbered dir with a cleanup lock and remove old ones."""
|
||||||
e = None
|
e = None
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
try:
|
try:
|
||||||
|
@ -381,17 +378,18 @@ def resolve_from_str(input: str, root: py.path.local) -> Path:
|
||||||
|
|
||||||
|
|
||||||
def fnmatch_ex(pattern: str, path) -> bool:
|
def fnmatch_ex(pattern: str, path) -> bool:
|
||||||
"""FNMatcher port from py.path.common which works with PurePath() instances.
|
"""A port of FNMatcher from py.path.common which works with PurePath() instances.
|
||||||
|
|
||||||
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
|
The difference between this algorithm and PurePath.match() is that the
|
||||||
for each part of the path, while this algorithm uses the whole path instead.
|
latter matches "**" glob expressions for each part of the path, while
|
||||||
|
this algorithm uses the whole path instead.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
"tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
|
"tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py"
|
||||||
PurePath.match().
|
with this algorithm, but not with PurePath.match().
|
||||||
|
|
||||||
This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
|
This algorithm was ported to keep backward-compatibility with existing
|
||||||
this logic.
|
settings which assume paths match according this logic.
|
||||||
|
|
||||||
References:
|
References:
|
||||||
* https://bugs.python.org/issue29249
|
* https://bugs.python.org/issue29249
|
||||||
|
@ -421,7 +419,7 @@ def parts(s: str) -> Set[str]:
|
||||||
|
|
||||||
|
|
||||||
def symlink_or_skip(src, dst, **kwargs):
|
def symlink_or_skip(src, dst, **kwargs):
|
||||||
"""Makes a symlink or skips the test in case symlinks are not supported."""
|
"""Make a symlink, or skip the test in case symlinks are not supported."""
|
||||||
try:
|
try:
|
||||||
os.symlink(str(src), str(dst), **kwargs)
|
os.symlink(str(src), str(dst), **kwargs)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
|
@ -429,7 +427,7 @@ def symlink_or_skip(src, dst, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
class ImportMode(Enum):
|
class ImportMode(Enum):
|
||||||
"""Possible values for `mode` parameter of `import_path`"""
|
"""Possible values for `mode` parameter of `import_path`."""
|
||||||
|
|
||||||
prepend = "prepend"
|
prepend = "prepend"
|
||||||
append = "append"
|
append = "append"
|
||||||
|
@ -450,8 +448,7 @@ def import_path(
|
||||||
*,
|
*,
|
||||||
mode: Union[str, ImportMode] = ImportMode.prepend
|
mode: Union[str, ImportMode] = ImportMode.prepend
|
||||||
) -> ModuleType:
|
) -> ModuleType:
|
||||||
"""
|
"""Import and return a module from the given path, which can be a file (a module) or
|
||||||
Imports and returns a module from the given path, which can be a file (a module) or
|
|
||||||
a directory (a package).
|
a directory (a package).
|
||||||
|
|
||||||
The import mechanism used is controlled by the `mode` parameter:
|
The import mechanism used is controlled by the `mode` parameter:
|
||||||
|
@ -467,7 +464,8 @@ def import_path(
|
||||||
to import the module, which avoids having to use `__import__` and muck with `sys.path`
|
to import the module, which avoids having to use `__import__` and muck with `sys.path`
|
||||||
at all. It effectively allows having same-named test modules in different places.
|
at all. It effectively allows having same-named test modules in different places.
|
||||||
|
|
||||||
:raise ImportPathMismatchError: if after importing the given `path` and the module `__file__`
|
:raises ImportPathMismatchError:
|
||||||
|
If after importing the given `path` and the module `__file__`
|
||||||
are different. Only raised in `prepend` and `append` modes.
|
are different. Only raised in `prepend` and `append` modes.
|
||||||
"""
|
"""
|
||||||
mode = ImportMode(mode)
|
mode = ImportMode(mode)
|
||||||
|
@ -506,7 +504,7 @@ def import_path(
|
||||||
pkg_root = path.parent
|
pkg_root = path.parent
|
||||||
module_name = path.stem
|
module_name = path.stem
|
||||||
|
|
||||||
# change sys.path permanently: restoring it at the end of this function would cause surprising
|
# Change sys.path permanently: restoring it at the end of this function would cause surprising
|
||||||
# problems because of delayed imports: for example, a conftest.py file imported by this function
|
# problems because of delayed imports: for example, a conftest.py file imported by this function
|
||||||
# might have local imports, which would fail at runtime if we restored sys.path.
|
# might have local imports, which would fail at runtime if we restored sys.path.
|
||||||
if mode is ImportMode.append:
|
if mode is ImportMode.append:
|
||||||
|
@ -546,7 +544,8 @@ def import_path(
|
||||||
def resolve_package_path(path: Path) -> Optional[Path]:
|
def resolve_package_path(path: Path) -> Optional[Path]:
|
||||||
"""Return the Python package path by looking for the last
|
"""Return the Python package path by looking for the last
|
||||||
directory upwards which still contains an __init__.py.
|
directory upwards which still contains an __init__.py.
|
||||||
Return None if it can not be determined.
|
|
||||||
|
Returns None if it can not be determined.
|
||||||
"""
|
"""
|
||||||
result = None
|
result = None
|
||||||
for parent in itertools.chain((path,), path.parents):
|
for parent in itertools.chain((path,), path.parents):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""(disabled by default) support for testing pytest and pytest plugins."""
|
"""(Disabled by default) support for testing pytest and pytest plugins."""
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import gc
|
import gc
|
||||||
import importlib
|
import importlib
|
||||||
|
@ -166,9 +166,7 @@ class LsofFdLeakChecker:
|
||||||
def _pytest(request: FixtureRequest) -> "PytestArg":
|
def _pytest(request: FixtureRequest) -> "PytestArg":
|
||||||
"""Return a helper which offers a gethookrecorder(hook) method which
|
"""Return a helper which offers a gethookrecorder(hook) method which
|
||||||
returns a HookRecorder instance which helps to make assertions about called
|
returns a HookRecorder instance which helps to make assertions about called
|
||||||
hooks.
|
hooks."""
|
||||||
|
|
||||||
"""
|
|
||||||
return PytestArg(request)
|
return PytestArg(request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -208,7 +206,6 @@ class HookRecorder:
|
||||||
|
|
||||||
This wraps all the hook calls in the plugin manager, recording each call
|
This wraps all the hook calls in the plugin manager, recording each call
|
||||||
before propagating the normal calls.
|
before propagating the normal calls.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pluginmanager: PytestPluginManager) -> None:
|
def __init__(self, pluginmanager: PytestPluginManager) -> None:
|
||||||
|
@ -285,7 +282,7 @@ class HookRecorder:
|
||||||
] = "pytest_runtest_logreport pytest_collectreport",
|
] = "pytest_runtest_logreport pytest_collectreport",
|
||||||
when=None,
|
when=None,
|
||||||
):
|
):
|
||||||
"""return a testreport whose dotted import path matches"""
|
"""Return a testreport whose dotted import path matches."""
|
||||||
values = []
|
values = []
|
||||||
for rep in self.getreports(names=names):
|
for rep in self.getreports(names=names):
|
||||||
if not when and rep.when != "call" and rep.passed:
|
if not when and rep.when != "call" and rep.passed:
|
||||||
|
@ -358,17 +355,14 @@ class HookRecorder:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def linecomp() -> "LineComp":
|
def linecomp() -> "LineComp":
|
||||||
"""
|
"""A :class: `LineComp` instance for checking that an input linearly
|
||||||
A :class: `LineComp` instance for checking that an input linearly
|
contains a sequence of strings."""
|
||||||
contains a sequence of strings.
|
|
||||||
"""
|
|
||||||
return LineComp()
|
return LineComp()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="LineMatcher")
|
@pytest.fixture(name="LineMatcher")
|
||||||
def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]":
|
def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]":
|
||||||
"""
|
"""A reference to the :class: `LineMatcher`.
|
||||||
A reference to the :class: `LineMatcher`.
|
|
||||||
|
|
||||||
This is instantiable with a list of lines (without their trailing newlines).
|
This is instantiable with a list of lines (without their trailing newlines).
|
||||||
This is useful for testing large texts, such as the output of commands.
|
This is useful for testing large texts, such as the output of commands.
|
||||||
|
@ -378,12 +372,10 @@ def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]":
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir":
|
def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir":
|
||||||
"""
|
"""A :class: `TestDir` instance, that can be used to run and test pytest itself.
|
||||||
A :class: `TestDir` instance, that can be used to run and test pytest itself.
|
|
||||||
|
|
||||||
It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture
|
It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture
|
||||||
but provides methods which aid in testing pytest itself.
|
but provides methods which aid in testing pytest itself.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return Testdir(request, tmpdir_factory)
|
return Testdir(request, tmpdir_factory)
|
||||||
|
|
||||||
|
@ -406,9 +398,9 @@ def _config_for_test() -> Generator[Config, None, None]:
|
||||||
config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
|
config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
|
||||||
|
|
||||||
|
|
||||||
# regex to match the session duration string in the summary: "74.34s"
|
# Regex to match the session duration string in the summary: "74.34s".
|
||||||
rex_session_duration = re.compile(r"\d+\.\d\ds")
|
rex_session_duration = re.compile(r"\d+\.\d\ds")
|
||||||
# regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped"
|
# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped".
|
||||||
rex_outcome = re.compile(r"(\d+) (\w+)")
|
rex_outcome = re.compile(r"(\d+) (\w+)")
|
||||||
|
|
||||||
|
|
||||||
|
@ -424,13 +416,13 @@ class RunResult:
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode]
|
self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode]
|
||||||
"""the return value"""
|
"""The return value."""
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.ret = ret
|
self.ret = ret
|
||||||
self.outlines = outlines
|
self.outlines = outlines
|
||||||
"""list of lines captured from stdout"""
|
"""List of lines captured from stdout."""
|
||||||
self.errlines = errlines
|
self.errlines = errlines
|
||||||
"""list of lines captured from stderr"""
|
"""List of lines captured from stderr."""
|
||||||
self.stdout = LineMatcher(outlines)
|
self.stdout = LineMatcher(outlines)
|
||||||
""":class:`LineMatcher` of stdout.
|
""":class:`LineMatcher` of stdout.
|
||||||
|
|
||||||
|
@ -438,9 +430,9 @@ class RunResult:
|
||||||
:func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method.
|
:func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method.
|
||||||
"""
|
"""
|
||||||
self.stderr = LineMatcher(errlines)
|
self.stderr = LineMatcher(errlines)
|
||||||
""":class:`LineMatcher` of stderr"""
|
""":class:`LineMatcher` of stderr."""
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
"""duration in seconds"""
|
"""Duration in seconds."""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
|
@ -456,19 +448,19 @@ class RunResult:
|
||||||
|
|
||||||
======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
|
======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
|
||||||
|
|
||||||
Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``
|
Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
|
||||||
"""
|
"""
|
||||||
return self.parse_summary_nouns(self.outlines)
|
return self.parse_summary_nouns(self.outlines)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_summary_nouns(cls, lines) -> Dict[str, int]:
|
def parse_summary_nouns(cls, lines) -> Dict[str, int]:
|
||||||
"""Extracts the nouns from a pytest terminal summary line.
|
"""Extract the nouns from a pytest terminal summary line.
|
||||||
|
|
||||||
It always returns the plural noun for consistency::
|
It always returns the plural noun for consistency::
|
||||||
|
|
||||||
======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
|
======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
|
||||||
|
|
||||||
Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``
|
Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
|
||||||
"""
|
"""
|
||||||
for line in reversed(lines):
|
for line in reversed(lines):
|
||||||
if rex_session_duration.search(line):
|
if rex_session_duration.search(line):
|
||||||
|
@ -494,8 +486,7 @@ class RunResult:
|
||||||
xfailed: int = 0,
|
xfailed: int = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Assert that the specified outcomes appear with the respective
|
"""Assert that the specified outcomes appear with the respective
|
||||||
numbers (0 means it didn't occur) in the text output from a test run.
|
numbers (0 means it didn't occur) in the text output from a test run."""
|
||||||
"""
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
d = self.parseoutcomes()
|
d = self.parseoutcomes()
|
||||||
|
@ -551,7 +542,7 @@ class SysPathsSnapshot:
|
||||||
class Testdir:
|
class Testdir:
|
||||||
"""Temporary test directory with tools to test/run pytest itself.
|
"""Temporary test directory with tools to test/run pytest itself.
|
||||||
|
|
||||||
This is based on the ``tmpdir`` fixture but provides a number of methods
|
This is based on the :fixture:`tmpdir` fixture but provides a number of methods
|
||||||
which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
|
which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
|
||||||
methods will use :py:attr:`tmpdir` as their current working directory.
|
methods will use :py:attr:`tmpdir` as their current working directory.
|
||||||
|
|
||||||
|
@ -559,11 +550,11 @@ class Testdir:
|
||||||
|
|
||||||
:ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
|
:ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
|
||||||
|
|
||||||
:ivar plugins: A list of plugins to use with :py:meth:`parseconfig` and
|
:ivar plugins:
|
||||||
|
A list of plugins to use with :py:meth:`parseconfig` and
|
||||||
:py:meth:`runpytest`. Initially this is an empty list but plugins can
|
:py:meth:`runpytest`. Initially this is an empty list but plugins can
|
||||||
be added to the list. The type of items to add to the list depends on
|
be added to the list. The type of items to add to the list depends on
|
||||||
the method using them so refer to them for details.
|
the method using them so refer to them for details.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__test__ = False
|
__test__ = False
|
||||||
|
@ -618,7 +609,6 @@ class Testdir:
|
||||||
Some methods modify the global interpreter state and this tries to
|
Some methods modify the global interpreter state and this tries to
|
||||||
clean this up. It does not remove the temporary directory however so
|
clean this up. It does not remove the temporary directory however so
|
||||||
it can be looked at after the test run has finished.
|
it can be looked at after the test run has finished.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._sys_modules_snapshot.restore()
|
self._sys_modules_snapshot.restore()
|
||||||
self._sys_path_snapshot.restore()
|
self._sys_path_snapshot.restore()
|
||||||
|
@ -626,9 +616,9 @@ class Testdir:
|
||||||
self.monkeypatch.undo()
|
self.monkeypatch.undo()
|
||||||
|
|
||||||
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
|
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
|
||||||
# some zope modules used by twisted-related tests keep internal state
|
# Some zope modules used by twisted-related tests keep internal state
|
||||||
# and can't be deleted; we had some trouble in the past with
|
# and can't be deleted; we had some trouble in the past with
|
||||||
# `zope.interface` for example
|
# `zope.interface` for example.
|
||||||
def preserve_module(name):
|
def preserve_module(name):
|
||||||
return name.startswith("zope")
|
return name.startswith("zope")
|
||||||
|
|
||||||
|
@ -644,7 +634,6 @@ class Testdir:
|
||||||
"""Cd into the temporary directory.
|
"""Cd into the temporary directory.
|
||||||
|
|
||||||
This is done automatically upon instantiation.
|
This is done automatically upon instantiation.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.tmpdir.chdir()
|
self.tmpdir.chdir()
|
||||||
|
|
||||||
|
@ -673,12 +662,15 @@ class Testdir:
|
||||||
def makefile(self, ext: str, *args: str, **kwargs):
|
def makefile(self, ext: str, *args: str, **kwargs):
|
||||||
r"""Create new file(s) in the testdir.
|
r"""Create new file(s) in the testdir.
|
||||||
|
|
||||||
:param str ext: The extension the file(s) should use, including the dot, e.g. `.py`.
|
:param str ext:
|
||||||
:param list[str] args: All args will be treated as strings and joined using newlines.
|
The extension the file(s) should use, including the dot, e.g. `.py`.
|
||||||
The result will be written as contents to the file. The name of the
|
:param args:
|
||||||
file will be based on the test function requesting this fixture.
|
All args are treated as strings and joined using newlines.
|
||||||
:param kwargs: Each keyword is the name of a file, while the value of it will
|
The result is written as contents to the file. The name of the
|
||||||
be written as contents of the file.
|
file is based on the test function requesting this fixture.
|
||||||
|
:param kwargs:
|
||||||
|
Each keyword is the name of a file, while the value of it will
|
||||||
|
be written as contents of the file.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
|
@ -713,6 +705,7 @@ class Testdir:
|
||||||
|
|
||||||
def makepyfile(self, *args, **kwargs):
|
def makepyfile(self, *args, **kwargs):
|
||||||
r"""Shortcut for .makefile() with a .py extension.
|
r"""Shortcut for .makefile() with a .py extension.
|
||||||
|
|
||||||
Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
|
Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
|
||||||
existing files.
|
existing files.
|
||||||
|
|
||||||
|
@ -721,17 +714,18 @@ class Testdir:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_something(testdir):
|
def test_something(testdir):
|
||||||
# initial file is created test_something.py
|
# Initial file is created test_something.py.
|
||||||
testdir.makepyfile("foobar")
|
testdir.makepyfile("foobar")
|
||||||
# to create multiple files, pass kwargs accordingly
|
# To create multiple files, pass kwargs accordingly.
|
||||||
testdir.makepyfile(custom="foobar")
|
testdir.makepyfile(custom="foobar")
|
||||||
# at this point, both 'test_something.py' & 'custom.py' exist in the test directory
|
# At this point, both 'test_something.py' & 'custom.py' exist in the test directory.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._makefile(".py", args, kwargs)
|
return self._makefile(".py", args, kwargs)
|
||||||
|
|
||||||
def maketxtfile(self, *args, **kwargs):
|
def maketxtfile(self, *args, **kwargs):
|
||||||
r"""Shortcut for .makefile() with a .txt extension.
|
r"""Shortcut for .makefile() with a .txt extension.
|
||||||
|
|
||||||
Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
|
Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
|
||||||
existing files.
|
existing files.
|
||||||
|
|
||||||
|
@ -740,11 +734,11 @@ class Testdir:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_something(testdir):
|
def test_something(testdir):
|
||||||
# initial file is created test_something.txt
|
# Initial file is created test_something.txt.
|
||||||
testdir.maketxtfile("foobar")
|
testdir.maketxtfile("foobar")
|
||||||
# to create multiple files, pass kwargs accordingly
|
# To create multiple files, pass kwargs accordingly.
|
||||||
testdir.maketxtfile(custom="foobar")
|
testdir.maketxtfile(custom="foobar")
|
||||||
# at this point, both 'test_something.txt' & 'custom.txt' exist in the test directory
|
# At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self._makefile(".txt", args, kwargs)
|
return self._makefile(".txt", args, kwargs)
|
||||||
|
@ -765,11 +759,10 @@ class Testdir:
|
||||||
return self.tmpdir.mkdir(name)
|
return self.tmpdir.mkdir(name)
|
||||||
|
|
||||||
def mkpydir(self, name) -> py.path.local:
|
def mkpydir(self, name) -> py.path.local:
|
||||||
"""Create a new python package.
|
"""Create a new Python package.
|
||||||
|
|
||||||
This creates a (sub)directory with an empty ``__init__.py`` file so it
|
This creates a (sub)directory with an empty ``__init__.py`` file so it
|
||||||
gets recognised as a python package.
|
gets recognised as a Python package.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
p = self.mkdir(name)
|
p = self.mkdir(name)
|
||||||
p.ensure("__init__.py")
|
p.ensure("__init__.py")
|
||||||
|
@ -779,8 +772,7 @@ class Testdir:
|
||||||
"""Copy file from project's directory into the testdir.
|
"""Copy file from project's directory into the testdir.
|
||||||
|
|
||||||
:param str name: The name of the file to copy.
|
:param str name: The name of the file to copy.
|
||||||
:return: path to the copied directory (inside ``self.tmpdir``).
|
:returns: Path to the copied directory (inside ``self.tmpdir``).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import warnings
|
import warnings
|
||||||
from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
|
from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
|
||||||
|
@ -830,12 +822,11 @@ class Testdir:
|
||||||
def getnode(self, config: Config, arg):
|
def getnode(self, config: Config, arg):
|
||||||
"""Return the collection node of a file.
|
"""Return the collection node of a file.
|
||||||
|
|
||||||
:param config: :py:class:`_pytest.config.Config` instance, see
|
:param _pytest.config.Config config:
|
||||||
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the
|
A pytest config.
|
||||||
configuration
|
See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
|
||||||
|
:param py.path.local arg:
|
||||||
:param arg: a :py:class:`py.path.local` instance of the file
|
Path to the file.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
session = Session.from_config(config)
|
session = Session.from_config(config)
|
||||||
assert "::" not in str(arg)
|
assert "::" not in str(arg)
|
||||||
|
@ -851,8 +842,7 @@ class Testdir:
|
||||||
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
|
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
|
||||||
create the (configured) pytest Config instance.
|
create the (configured) pytest Config instance.
|
||||||
|
|
||||||
:param path: a :py:class:`py.path.local` instance of the file
|
:param py.path.local path: Path to the file.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
config = self.parseconfigure(path)
|
config = self.parseconfigure(path)
|
||||||
session = Session.from_config(config)
|
session = Session.from_config(config)
|
||||||
|
@ -867,7 +857,6 @@ class Testdir:
|
||||||
|
|
||||||
This recurses into the collection node and returns a list of all the
|
This recurses into the collection node and returns a list of all the
|
||||||
test items contained within.
|
test items contained within.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
session = colitems[0].session
|
session = colitems[0].session
|
||||||
result = [] # type: List[Item]
|
result = [] # type: List[Item]
|
||||||
|
@ -882,7 +871,6 @@ class Testdir:
|
||||||
provide a ``.getrunner()`` method which should return a runner which
|
provide a ``.getrunner()`` method which should return a runner which
|
||||||
can run the test protocol for a single item, e.g.
|
can run the test protocol for a single item, e.g.
|
||||||
:py:func:`_pytest.runner.runtestprotocol`.
|
:py:func:`_pytest.runner.runtestprotocol`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# used from runner functional tests
|
# used from runner functional tests
|
||||||
item = self.getitem(source)
|
item = self.getitem(source)
|
||||||
|
@ -898,12 +886,11 @@ class Testdir:
|
||||||
``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
|
``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
|
||||||
for the result.
|
for the result.
|
||||||
|
|
||||||
:param source: the source code of the test module
|
:param source: The source code of the test module.
|
||||||
|
|
||||||
:param cmdlineargs: any extra command line arguments to use
|
:param cmdlineargs: Any extra command line arguments to use.
|
||||||
|
|
||||||
:return: :py:class:`HookRecorder` instance of the result
|
|
||||||
|
|
||||||
|
:returns: :py:class:`HookRecorder` instance of the result.
|
||||||
"""
|
"""
|
||||||
p = self.makepyfile(source)
|
p = self.makepyfile(source)
|
||||||
values = list(cmdlineargs) + [p]
|
values = list(cmdlineargs) + [p]
|
||||||
|
@ -915,7 +902,6 @@ class Testdir:
|
||||||
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
Runs the :py:func:`pytest.main` function to run all of pytest inside
|
||||||
the test process itself like :py:meth:`inline_run`, but returns a
|
the test process itself like :py:meth:`inline_run`, but returns a
|
||||||
tuple of the collected items and a :py:class:`HookRecorder` instance.
|
tuple of the collected items and a :py:class:`HookRecorder` instance.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
rec = self.inline_run("--collect-only", *args)
|
rec = self.inline_run("--collect-only", *args)
|
||||||
items = [x.item for x in rec.getcalls("pytest_itemcollected")]
|
items = [x.item for x in rec.getcalls("pytest_itemcollected")]
|
||||||
|
@ -930,14 +916,15 @@ class Testdir:
|
||||||
from that run than can be done by matching stdout/stderr from
|
from that run than can be done by matching stdout/stderr from
|
||||||
:py:meth:`runpytest`.
|
:py:meth:`runpytest`.
|
||||||
|
|
||||||
:param args: command line arguments to pass to :py:func:`pytest.main`
|
:param args:
|
||||||
|
Command line arguments to pass to :py:func:`pytest.main`.
|
||||||
:kwarg plugins: extra plugin instances the ``pytest.main()`` instance should use.
|
:param plugins:
|
||||||
|
Extra plugin instances the ``pytest.main()`` instance should use.
|
||||||
:kwarg no_reraise_ctrlc: typically we reraise keyboard interrupts from the child run. If
|
:param no_reraise_ctrlc:
|
||||||
|
Typically we reraise keyboard interrupts from the child run. If
|
||||||
True, the KeyboardInterrupt exception is captured.
|
True, the KeyboardInterrupt exception is captured.
|
||||||
|
|
||||||
:return: a :py:class:`HookRecorder` instance
|
:returns: A :py:class:`HookRecorder` instance.
|
||||||
"""
|
"""
|
||||||
# (maybe a cpython bug?) the importlib cache sometimes isn't updated
|
# (maybe a cpython bug?) the importlib cache sometimes isn't updated
|
||||||
# properly between file creation and inline_run (especially if imports
|
# properly between file creation and inline_run (especially if imports
|
||||||
|
@ -977,8 +964,8 @@ class Testdir:
|
||||||
|
|
||||||
reprec.ret = ret # type: ignore[attr-defined]
|
reprec.ret = ret # type: ignore[attr-defined]
|
||||||
|
|
||||||
# typically we reraise keyboard interrupts from the child run
|
# Typically we reraise keyboard interrupts from the child run
|
||||||
# because it's our user requesting interruption of the testing
|
# because it's our user requesting interruption of the testing.
|
||||||
if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
|
if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
|
||||||
calls = reprec.getcalls("pytest_keyboard_interrupt")
|
calls = reprec.getcalls("pytest_keyboard_interrupt")
|
||||||
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
|
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
|
||||||
|
@ -990,8 +977,7 @@ class Testdir:
|
||||||
|
|
||||||
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
|
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
|
||||||
"""Return result of running pytest in-process, providing a similar
|
"""Return result of running pytest in-process, providing a similar
|
||||||
interface to what self.runpytest() provides.
|
interface to what self.runpytest() provides."""
|
||||||
"""
|
|
||||||
syspathinsert = kwargs.pop("syspathinsert", False)
|
syspathinsert = kwargs.pop("syspathinsert", False)
|
||||||
|
|
||||||
if syspathinsert:
|
if syspathinsert:
|
||||||
|
@ -1032,9 +1018,7 @@ class Testdir:
|
||||||
|
|
||||||
def runpytest(self, *args, **kwargs) -> RunResult:
|
def runpytest(self, *args, **kwargs) -> RunResult:
|
||||||
"""Run pytest inline or in a subprocess, depending on the command line
|
"""Run pytest inline or in a subprocess, depending on the command line
|
||||||
option "--runpytest" and return a :py:class:`RunResult`.
|
option "--runpytest" and return a :py:class:`RunResult`."""
|
||||||
|
|
||||||
"""
|
|
||||||
args = self._ensure_basetemp(args)
|
args = self._ensure_basetemp(args)
|
||||||
if self._method == "inprocess":
|
if self._method == "inprocess":
|
||||||
return self.runpytest_inprocess(*args, **kwargs)
|
return self.runpytest_inprocess(*args, **kwargs)
|
||||||
|
@ -1061,7 +1045,6 @@ class Testdir:
|
||||||
|
|
||||||
If :py:attr:`plugins` has been populated they should be plugin modules
|
If :py:attr:`plugins` has been populated they should be plugin modules
|
||||||
to be registered with the PluginManager.
|
to be registered with the PluginManager.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
args = self._ensure_basetemp(args)
|
args = self._ensure_basetemp(args)
|
||||||
|
|
||||||
|
@ -1077,7 +1060,7 @@ class Testdir:
|
||||||
def parseconfigure(self, *args) -> Config:
|
def parseconfigure(self, *args) -> Config:
|
||||||
"""Return a new pytest configured Config instance.
|
"""Return a new pytest configured Config instance.
|
||||||
|
|
||||||
This returns a new :py:class:`_pytest.config.Config` instance like
|
Returns a new :py:class:`_pytest.config.Config` instance like
|
||||||
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
|
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
|
||||||
"""
|
"""
|
||||||
config = self.parseconfig(*args)
|
config = self.parseconfig(*args)
|
||||||
|
@ -1087,15 +1070,14 @@ class Testdir:
|
||||||
def getitem(self, source, funcname: str = "test_func") -> Item:
|
def getitem(self, source, funcname: str = "test_func") -> Item:
|
||||||
"""Return the test item for a test function.
|
"""Return the test item for a test function.
|
||||||
|
|
||||||
This writes the source to a python file and runs pytest's collection on
|
Writes the source to a python file and runs pytest's collection on
|
||||||
the resulting module, returning the test item for the requested
|
the resulting module, returning the test item for the requested
|
||||||
function name.
|
function name.
|
||||||
|
|
||||||
:param source: the module source
|
:param source:
|
||||||
|
The module source.
|
||||||
:param funcname: the name of the test function for which to return a
|
:param funcname:
|
||||||
test item
|
The name of the test function for which to return a test item.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
items = self.getitems(source)
|
items = self.getitems(source)
|
||||||
for item in items:
|
for item in items:
|
||||||
|
@ -1108,9 +1090,8 @@ class Testdir:
|
||||||
def getitems(self, source) -> List[Item]:
|
def getitems(self, source) -> List[Item]:
|
||||||
"""Return all test items collected from the module.
|
"""Return all test items collected from the module.
|
||||||
|
|
||||||
This writes the source to a python file and runs pytest's collection on
|
Writes the source to a Python file and runs pytest's collection on
|
||||||
the resulting module, returning all test items contained within.
|
the resulting module, returning all test items contained within.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
modcol = self.getmodulecol(source)
|
modcol = self.getmodulecol(source)
|
||||||
return self.genitems([modcol])
|
return self.genitems([modcol])
|
||||||
|
@ -1118,18 +1099,19 @@ class Testdir:
|
||||||
def getmodulecol(self, source, configargs=(), withinit: bool = False):
|
def getmodulecol(self, source, configargs=(), withinit: bool = False):
|
||||||
"""Return the module collection node for ``source``.
|
"""Return the module collection node for ``source``.
|
||||||
|
|
||||||
This writes ``source`` to a file using :py:meth:`makepyfile` and then
|
Writes ``source`` to a file using :py:meth:`makepyfile` and then
|
||||||
runs the pytest collection on it, returning the collection node for the
|
runs the pytest collection on it, returning the collection node for the
|
||||||
test module.
|
test module.
|
||||||
|
|
||||||
:param source: the source code of the module to collect
|
:param source:
|
||||||
|
The source code of the module to collect.
|
||||||
|
|
||||||
:param configargs: any extra arguments to pass to
|
:param configargs:
|
||||||
:py:meth:`parseconfigure`
|
Any extra arguments to pass to :py:meth:`parseconfigure`.
|
||||||
|
|
||||||
:param withinit: whether to also write an ``__init__.py`` file to the
|
|
||||||
same directory to ensure it is a package
|
|
||||||
|
|
||||||
|
:param withinit:
|
||||||
|
Whether to also write an ``__init__.py`` file to the same
|
||||||
|
directory to ensure it is a package.
|
||||||
"""
|
"""
|
||||||
if isinstance(source, Path):
|
if isinstance(source, Path):
|
||||||
path = self.tmpdir.join(str(source))
|
path = self.tmpdir.join(str(source))
|
||||||
|
@ -1147,12 +1129,11 @@ class Testdir:
|
||||||
) -> Optional[Union[Item, Collector]]:
|
) -> Optional[Union[Item, Collector]]:
|
||||||
"""Return the collection node for name from the module collection.
|
"""Return the collection node for name from the module collection.
|
||||||
|
|
||||||
This will search a module collection node for a collection node
|
Searchs a module collection node for a collection node matching the
|
||||||
matching the given name.
|
given name.
|
||||||
|
|
||||||
:param modcol: a module collection node; see :py:meth:`getmodulecol`
|
:param modcol: A module collection node; see :py:meth:`getmodulecol`.
|
||||||
|
:param name: The name of the node to return.
|
||||||
:param name: the name of the node to return
|
|
||||||
"""
|
"""
|
||||||
if modcol not in self._mod_collections:
|
if modcol not in self._mod_collections:
|
||||||
self._mod_collections[modcol] = list(modcol.collect())
|
self._mod_collections[modcol] = list(modcol.collect())
|
||||||
|
@ -1171,11 +1152,10 @@ class Testdir:
|
||||||
):
|
):
|
||||||
"""Invoke subprocess.Popen.
|
"""Invoke subprocess.Popen.
|
||||||
|
|
||||||
This calls subprocess.Popen making sure the current working directory
|
Calls subprocess.Popen making sure the current working directory is
|
||||||
is in the PYTHONPATH.
|
in the PYTHONPATH.
|
||||||
|
|
||||||
You probably want to use :py:meth:`run` instead.
|
You probably want to use :py:meth:`run` instead.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["PYTHONPATH"] = os.pathsep.join(
|
env["PYTHONPATH"] = os.pathsep.join(
|
||||||
|
@ -1207,16 +1187,18 @@ class Testdir:
|
||||||
|
|
||||||
Run a process using subprocess.Popen saving the stdout and stderr.
|
Run a process using subprocess.Popen saving the stdout and stderr.
|
||||||
|
|
||||||
:param args: the sequence of arguments to pass to `subprocess.Popen()`
|
:param args:
|
||||||
:kwarg timeout: the period in seconds after which to timeout and raise
|
The sequence of arguments to pass to `subprocess.Popen()`.
|
||||||
:py:class:`Testdir.TimeoutExpired`
|
:param timeout:
|
||||||
:kwarg stdin: optional standard input. Bytes are being send, closing
|
The period in seconds after which to timeout and raise
|
||||||
|
:py:class:`Testdir.TimeoutExpired`.
|
||||||
|
:param stdin:
|
||||||
|
Optional standard input. Bytes are being send, closing
|
||||||
the pipe, otherwise it is passed through to ``popen``.
|
the pipe, otherwise it is passed through to ``popen``.
|
||||||
Defaults to ``CLOSE_STDIN``, which translates to using a pipe
|
Defaults to ``CLOSE_STDIN``, which translates to using a pipe
|
||||||
(``subprocess.PIPE``) that gets closed.
|
(``subprocess.PIPE``) that gets closed.
|
||||||
|
|
||||||
Returns a :py:class:`RunResult`.
|
:rtype: RunResult
|
||||||
|
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
|
@ -1292,13 +1274,15 @@ class Testdir:
|
||||||
def runpython(self, script) -> RunResult:
|
def runpython(self, script) -> RunResult:
|
||||||
"""Run a python script using sys.executable as interpreter.
|
"""Run a python script using sys.executable as interpreter.
|
||||||
|
|
||||||
Returns a :py:class:`RunResult`.
|
:rtype: RunResult
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.run(sys.executable, script)
|
return self.run(sys.executable, script)
|
||||||
|
|
||||||
def runpython_c(self, command):
|
def runpython_c(self, command):
|
||||||
"""Run python -c "command", return a :py:class:`RunResult`."""
|
"""Run python -c "command".
|
||||||
|
|
||||||
|
:rtype: RunResult
|
||||||
|
"""
|
||||||
return self.run(sys.executable, "-c", command)
|
return self.run(sys.executable, "-c", command)
|
||||||
|
|
||||||
def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunResult:
|
def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunResult:
|
||||||
|
@ -1310,11 +1294,13 @@ class Testdir:
|
||||||
with "runpytest-" to not conflict with the normal numbered pytest
|
with "runpytest-" to not conflict with the normal numbered pytest
|
||||||
location for temporary files and directories.
|
location for temporary files and directories.
|
||||||
|
|
||||||
:param args: the sequence of arguments to pass to the pytest subprocess
|
:param args:
|
||||||
:param timeout: the period in seconds after which to timeout and raise
|
The sequence of arguments to pass to the pytest subprocess.
|
||||||
:py:class:`Testdir.TimeoutExpired`
|
:param timeout:
|
||||||
|
The period in seconds after which to timeout and raise
|
||||||
|
:py:class:`Testdir.TimeoutExpired`.
|
||||||
|
|
||||||
Returns a :py:class:`RunResult`.
|
:rtype: RunResult
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-")
|
p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-")
|
||||||
|
@ -1334,7 +1320,6 @@ class Testdir:
|
||||||
directory locations.
|
directory locations.
|
||||||
|
|
||||||
The pexpect child is returned.
|
The pexpect child is returned.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
basetemp = self.tmpdir.mkdir("temp-pexpect")
|
basetemp = self.tmpdir.mkdir("temp-pexpect")
|
||||||
invoke = " ".join(map(str, self._getpytestargs()))
|
invoke = " ".join(map(str, self._getpytestargs()))
|
||||||
|
@ -1345,7 +1330,6 @@ class Testdir:
|
||||||
"""Run a command using pexpect.
|
"""Run a command using pexpect.
|
||||||
|
|
||||||
The pexpect child is returned.
|
The pexpect child is returned.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pexpect = pytest.importorskip("pexpect", "3.0")
|
pexpect = pytest.importorskip("pexpect", "3.0")
|
||||||
if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
|
if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
|
||||||
|
@ -1400,14 +1384,12 @@ class LineMatcher:
|
||||||
return lines2
|
return lines2
|
||||||
|
|
||||||
def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
|
def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
|
||||||
"""Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).
|
"""Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`)."""
|
||||||
"""
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._match_lines_random(lines2, fnmatch)
|
self._match_lines_random(lines2, fnmatch)
|
||||||
|
|
||||||
def re_match_lines_random(self, lines2: Sequence[str]) -> None:
|
def re_match_lines_random(self, lines2: Sequence[str]) -> None:
|
||||||
"""Check lines exist in the output in any order (using :func:`python:re.match`).
|
"""Check lines exist in the output in any order (using :func:`python:re.match`)."""
|
||||||
"""
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
|
self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
|
||||||
|
|
||||||
|
@ -1452,8 +1434,8 @@ class LineMatcher:
|
||||||
wildcards. If they do not match a pytest.fail() is called. The
|
wildcards. If they do not match a pytest.fail() is called. The
|
||||||
matches and non-matches are also shown as part of the error message.
|
matches and non-matches are also shown as part of the error message.
|
||||||
|
|
||||||
:param lines2: string patterns to match.
|
:param lines2: String patterns to match.
|
||||||
:param consecutive: match lines consecutive?
|
:param consecutive: Match lines consecutively?
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
|
self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
|
||||||
|
@ -1489,14 +1471,18 @@ class LineMatcher:
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
|
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
|
||||||
|
|
||||||
:param list[str] lines2: list of string patterns to match. The actual
|
:param Sequence[str] lines2:
|
||||||
format depends on ``match_func``
|
List of string patterns to match. The actual format depends on
|
||||||
:param match_func: a callable ``match_func(line, pattern)`` where line
|
``match_func``.
|
||||||
is the captured line from stdout/stderr and pattern is the matching
|
:param match_func:
|
||||||
pattern
|
A callable ``match_func(line, pattern)`` where line is the
|
||||||
:param str match_nickname: the nickname for the match function that
|
captured line from stdout/stderr and pattern is the matching
|
||||||
will be logged to stdout when a match occurs
|
pattern.
|
||||||
:param consecutive: match lines consecutively?
|
:param str match_nickname:
|
||||||
|
The nickname for the match function that will be logged to stdout
|
||||||
|
when a match occurs.
|
||||||
|
:param consecutive:
|
||||||
|
Match lines consecutively?
|
||||||
"""
|
"""
|
||||||
if not isinstance(lines2, collections.abc.Sequence):
|
if not isinstance(lines2, collections.abc.Sequence):
|
||||||
raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__))
|
raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__))
|
||||||
|
@ -1546,7 +1532,7 @@ class LineMatcher:
|
||||||
def no_fnmatch_line(self, pat: str) -> None:
|
def no_fnmatch_line(self, pat: str) -> None:
|
||||||
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
|
"""Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
|
||||||
|
|
||||||
:param str pat: the pattern to match lines.
|
:param str pat: The pattern to match lines.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._no_match_line(pat, fnmatch, "fnmatch")
|
self._no_match_line(pat, fnmatch, "fnmatch")
|
||||||
|
@ -1554,7 +1540,7 @@ class LineMatcher:
|
||||||
def no_re_match_line(self, pat: str) -> None:
|
def no_re_match_line(self, pat: str) -> None:
|
||||||
"""Ensure captured lines do not match the given pattern, using ``re.match``.
|
"""Ensure captured lines do not match the given pattern, using ``re.match``.
|
||||||
|
|
||||||
:param str pat: the regular expression to match lines.
|
:param str pat: The regular expression to match lines.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
self._no_match_line(
|
self._no_match_line(
|
||||||
|
@ -1564,9 +1550,9 @@ class LineMatcher:
|
||||||
def _no_match_line(
|
def _no_match_line(
|
||||||
self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
|
self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``
|
"""Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch``.
|
||||||
|
|
||||||
:param str pat: the pattern to match lines
|
:param str pat: The pattern to match lines.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
nomatch_printed = False
|
nomatch_printed = False
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" Python test discovery, setup and run of test functions. """
|
"""Python test discovery, setup and run of test functions."""
|
||||||
import enum
|
import enum
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -201,7 +201,7 @@ def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]:
|
||||||
|
|
||||||
|
|
||||||
def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool:
|
def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool:
|
||||||
"""Returns True if path matches any of the patterns in the list of globs given."""
|
"""Return whether path matches any of the patterns in the list of globs given."""
|
||||||
return any(path.fnmatch(pattern) for pattern in patterns)
|
return any(path.fnmatch(pattern) for pattern in patterns)
|
||||||
|
|
||||||
|
|
||||||
|
@ -215,16 +215,16 @@ def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module":
|
||||||
|
|
||||||
@hookimpl(trylast=True)
|
@hookimpl(trylast=True)
|
||||||
def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
|
def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object):
|
||||||
# nothing was collected elsewhere, let's do it here
|
# Nothing was collected elsewhere, let's do it here.
|
||||||
if safe_isclass(obj):
|
if safe_isclass(obj):
|
||||||
if collector.istestclass(obj, name):
|
if collector.istestclass(obj, name):
|
||||||
return Class.from_parent(collector, name=name, obj=obj)
|
return Class.from_parent(collector, name=name, obj=obj)
|
||||||
elif collector.istestfunction(obj, name):
|
elif collector.istestfunction(obj, name):
|
||||||
# mock seems to store unbound methods (issue473), normalize it
|
# mock seems to store unbound methods (issue473), normalize it.
|
||||||
obj = getattr(obj, "__func__", obj)
|
obj = getattr(obj, "__func__", obj)
|
||||||
# We need to try and unwrap the function if it's a functools.partial
|
# We need to try and unwrap the function if it's a functools.partial
|
||||||
# or a functools.wrapped.
|
# or a functools.wrapped.
|
||||||
# We mustn't if it's been wrapped with mock.patch (python 2 only)
|
# We mustn't if it's been wrapped with mock.patch (python 2 only).
|
||||||
if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
|
if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
|
||||||
filename, lineno = getfslineno(obj)
|
filename, lineno = getfslineno(obj)
|
||||||
warnings.warn_explicit(
|
warnings.warn_explicit(
|
||||||
|
@ -298,14 +298,14 @@ class PyobjMixin:
|
||||||
self._obj = value
|
self._obj = value
|
||||||
|
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
"""Gets the underlying Python object. May be overwritten by subclasses."""
|
"""Get the underlying Python object. May be overwritten by subclasses."""
|
||||||
# TODO: Improve the type of `parent` such that assert/ignore aren't needed.
|
# TODO: Improve the type of `parent` such that assert/ignore aren't needed.
|
||||||
assert self.parent is not None
|
assert self.parent is not None
|
||||||
obj = self.parent.obj # type: ignore[attr-defined]
|
obj = self.parent.obj # type: ignore[attr-defined]
|
||||||
return getattr(obj, self.name)
|
return getattr(obj, self.name)
|
||||||
|
|
||||||
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
|
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
|
||||||
""" return python path relative to the containing module. """
|
"""Return Python path relative to the containing module."""
|
||||||
chain = self.listchain()
|
chain = self.listchain()
|
||||||
chain.reverse()
|
chain.reverse()
|
||||||
parts = []
|
parts = []
|
||||||
|
@ -346,8 +346,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
return self._matches_prefix_or_glob_option("python_functions", name)
|
return self._matches_prefix_or_glob_option("python_functions", name)
|
||||||
|
|
||||||
def isnosetest(self, obj: object) -> bool:
|
def isnosetest(self, obj: object) -> bool:
|
||||||
""" Look for the __test__ attribute, which is applied by the
|
"""Look for the __test__ attribute, which is applied by the
|
||||||
@nose.tools.istest decorator
|
@nose.tools.istest decorator.
|
||||||
"""
|
"""
|
||||||
# We explicitly check for "is True" here to not mistakenly treat
|
# We explicitly check for "is True" here to not mistakenly treat
|
||||||
# classes with a custom __getattr__ returning something truthy (like a
|
# classes with a custom __getattr__ returning something truthy (like a
|
||||||
|
@ -360,7 +360,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
def istestfunction(self, obj: object, name: str) -> bool:
|
def istestfunction(self, obj: object, name: str) -> bool:
|
||||||
if self.funcnamefilter(name) or self.isnosetest(obj):
|
if self.funcnamefilter(name) or self.isnosetest(obj):
|
||||||
if isinstance(obj, staticmethod):
|
if isinstance(obj, staticmethod):
|
||||||
# static methods need to be unwrapped
|
# staticmethods need to be unwrapped.
|
||||||
obj = safe_getattr(obj, "__func__", False)
|
obj = safe_getattr(obj, "__func__", False)
|
||||||
return (
|
return (
|
||||||
safe_getattr(obj, "__call__", False)
|
safe_getattr(obj, "__call__", False)
|
||||||
|
@ -373,16 +373,14 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
return self.classnamefilter(name) or self.isnosetest(obj)
|
return self.classnamefilter(name) or self.isnosetest(obj)
|
||||||
|
|
||||||
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
|
def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
|
||||||
"""
|
"""Check if the given name matches the prefix or glob-pattern defined
|
||||||
checks if the given name matches the prefix or glob-pattern defined
|
in ini configuration."""
|
||||||
in ini configuration.
|
|
||||||
"""
|
|
||||||
for option in self.config.getini(option_name):
|
for option in self.config.getini(option_name):
|
||||||
if name.startswith(option):
|
if name.startswith(option):
|
||||||
return True
|
return True
|
||||||
# check that name looks like a glob-string before calling fnmatch
|
# Check that name looks like a glob-string before calling fnmatch
|
||||||
# because this is called for every name in each collected module,
|
# because this is called for every name in each collected module,
|
||||||
# and fnmatch is somewhat expensive to call
|
# and fnmatch is somewhat expensive to call.
|
||||||
elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
|
elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
|
||||||
name, option
|
name, option
|
||||||
):
|
):
|
||||||
|
@ -457,10 +455,10 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
if not metafunc._calls:
|
if not metafunc._calls:
|
||||||
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
|
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
|
||||||
else:
|
else:
|
||||||
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
|
||||||
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
||||||
|
|
||||||
# add_funcarg_pseudo_fixture_def may have shadowed some fixtures
|
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
|
||||||
# with direct parametrization, so make sure we update what the
|
# with direct parametrization, so make sure we update what the
|
||||||
# function really needs.
|
# function really needs.
|
||||||
fixtureinfo.prune_dependency_tree()
|
fixtureinfo.prune_dependency_tree()
|
||||||
|
@ -479,7 +477,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||||
|
|
||||||
|
|
||||||
class Module(nodes.File, PyCollector):
|
class Module(nodes.File, PyCollector):
|
||||||
""" Collector for test classes and functions. """
|
"""Collector for test classes and functions."""
|
||||||
|
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
return self._importtestmodule()
|
return self._importtestmodule()
|
||||||
|
@ -491,7 +489,7 @@ class Module(nodes.File, PyCollector):
|
||||||
return super().collect()
|
return super().collect()
|
||||||
|
|
||||||
def _inject_setup_module_fixture(self) -> None:
|
def _inject_setup_module_fixture(self) -> None:
|
||||||
"""Injects a hidden autouse, module scoped fixture into the collected module object
|
"""Inject a hidden autouse, module scoped fixture into the collected module object
|
||||||
that invokes setUpModule/tearDownModule if either or both are available.
|
that invokes setUpModule/tearDownModule if either or both are available.
|
||||||
|
|
||||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||||
|
@ -518,7 +516,7 @@ class Module(nodes.File, PyCollector):
|
||||||
self.obj.__pytest_setup_module = xunit_setup_module_fixture
|
self.obj.__pytest_setup_module = xunit_setup_module_fixture
|
||||||
|
|
||||||
def _inject_setup_function_fixture(self) -> None:
|
def _inject_setup_function_fixture(self) -> None:
|
||||||
"""Injects a hidden autouse, function scoped fixture into the collected module object
|
"""Inject a hidden autouse, function scoped fixture into the collected module object
|
||||||
that invokes setup_function/teardown_function if either or both are available.
|
that invokes setup_function/teardown_function if either or both are available.
|
||||||
|
|
||||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||||
|
@ -547,7 +545,7 @@ class Module(nodes.File, PyCollector):
|
||||||
self.obj.__pytest_setup_function = xunit_setup_function_fixture
|
self.obj.__pytest_setup_function = xunit_setup_function_fixture
|
||||||
|
|
||||||
def _importtestmodule(self):
|
def _importtestmodule(self):
|
||||||
# we assume we are only called once per module
|
# We assume we are only called once per module.
|
||||||
importmode = self.config.getoption("--import-mode")
|
importmode = self.config.getoption("--import-mode")
|
||||||
try:
|
try:
|
||||||
mod = import_path(self.fspath, mode=importmode)
|
mod = import_path(self.fspath, mode=importmode)
|
||||||
|
@ -604,7 +602,7 @@ class Package(Module):
|
||||||
session=None,
|
session=None,
|
||||||
nodeid=None,
|
nodeid=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# NOTE: could be just the following, but kept as-is for compat.
|
# NOTE: Could be just the following, but kept as-is for compat.
|
||||||
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
||||||
session = parent.session
|
session = parent.session
|
||||||
nodes.FSCollector.__init__(
|
nodes.FSCollector.__init__(
|
||||||
|
@ -613,8 +611,8 @@ class Package(Module):
|
||||||
self.name = os.path.basename(str(fspath.dirname))
|
self.name = os.path.basename(str(fspath.dirname))
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
# not using fixtures to call setup_module here because autouse fixtures
|
# Not using fixtures to call setup_module here because autouse fixtures
|
||||||
# from packages are not called automatically (#4085)
|
# from packages are not called automatically (#4085).
|
||||||
setup_module = _get_first_non_fixture_func(
|
setup_module = _get_first_non_fixture_func(
|
||||||
self.obj, ("setUpModule", "setup_module")
|
self.obj, ("setUpModule", "setup_module")
|
||||||
)
|
)
|
||||||
|
@ -668,7 +666,7 @@ class Package(Module):
|
||||||
|
|
||||||
def _call_with_optional_argument(func, arg) -> None:
|
def _call_with_optional_argument(func, arg) -> None:
|
||||||
"""Call the given function with the given argument if func accepts one argument, otherwise
|
"""Call the given function with the given argument if func accepts one argument, otherwise
|
||||||
calls func without arguments"""
|
calls func without arguments."""
|
||||||
arg_count = func.__code__.co_argcount
|
arg_count = func.__code__.co_argcount
|
||||||
if inspect.ismethod(func):
|
if inspect.ismethod(func):
|
||||||
arg_count -= 1
|
arg_count -= 1
|
||||||
|
@ -680,9 +678,7 @@ def _call_with_optional_argument(func, arg) -> None:
|
||||||
|
|
||||||
def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
|
def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
|
||||||
"""Return the attribute from the given object to be used as a setup/teardown
|
"""Return the attribute from the given object to be used as a setup/teardown
|
||||||
xunit-style function, but only if not marked as a fixture to
|
xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
|
||||||
avoid calling it twice.
|
|
||||||
"""
|
|
||||||
for name in names:
|
for name in names:
|
||||||
meth = getattr(obj, name, None)
|
meth = getattr(obj, name, None)
|
||||||
if meth is not None and fixtures.getfixturemarker(meth) is None:
|
if meth is not None and fixtures.getfixturemarker(meth) is None:
|
||||||
|
@ -690,13 +686,11 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
|
||||||
|
|
||||||
|
|
||||||
class Class(PyCollector):
|
class Class(PyCollector):
|
||||||
""" Collector for test methods. """
|
"""Collector for test methods."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent, *, name, obj=None):
|
def from_parent(cls, parent, *, name, obj=None):
|
||||||
"""
|
"""The public constructor."""
|
||||||
The public constructor
|
|
||||||
"""
|
|
||||||
return super().from_parent(name=name, parent=parent)
|
return super().from_parent(name=name, parent=parent)
|
||||||
|
|
||||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||||
|
@ -729,7 +723,7 @@ class Class(PyCollector):
|
||||||
return [Instance.from_parent(self, name="()")]
|
return [Instance.from_parent(self, name="()")]
|
||||||
|
|
||||||
def _inject_setup_class_fixture(self) -> None:
|
def _inject_setup_class_fixture(self) -> None:
|
||||||
"""Injects a hidden autouse, class scoped fixture into the collected class object
|
"""Inject a hidden autouse, class scoped fixture into the collected class object
|
||||||
that invokes setup_class/teardown_class if either or both are available.
|
that invokes setup_class/teardown_class if either or both are available.
|
||||||
|
|
||||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||||
|
@ -753,7 +747,7 @@ class Class(PyCollector):
|
||||||
self.obj.__pytest_setup_class = xunit_setup_class_fixture
|
self.obj.__pytest_setup_class = xunit_setup_class_fixture
|
||||||
|
|
||||||
def _inject_setup_method_fixture(self) -> None:
|
def _inject_setup_method_fixture(self) -> None:
|
||||||
"""Injects a hidden autouse, function scoped fixture into the collected class object
|
"""Inject a hidden autouse, function scoped fixture into the collected class object
|
||||||
that invokes setup_method/teardown_method if either or both are available.
|
that invokes setup_method/teardown_method if either or both are available.
|
||||||
|
|
||||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||||
|
@ -780,9 +774,9 @@ class Class(PyCollector):
|
||||||
|
|
||||||
class Instance(PyCollector):
|
class Instance(PyCollector):
|
||||||
_ALLOW_MARKERS = False # hack, destroy later
|
_ALLOW_MARKERS = False # hack, destroy later
|
||||||
# instances share the object with their parents in a way
|
# Instances share the object with their parents in a way
|
||||||
# that duplicates markers instances if not taken out
|
# that duplicates markers instances if not taken out
|
||||||
# can be removed at node structure reorganization time
|
# can be removed at node structure reorganization time.
|
||||||
|
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
# TODO: Improve the type of `parent` such that assert/ignore aren't needed.
|
# TODO: Improve the type of `parent` such that assert/ignore aren't needed.
|
||||||
|
@ -874,8 +868,8 @@ class CallSpec2:
|
||||||
|
|
||||||
|
|
||||||
class Metafunc:
|
class Metafunc:
|
||||||
"""
|
"""Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
|
||||||
Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
|
|
||||||
They help to inspect a test function and to generate tests according to
|
They help to inspect a test function and to generate tests according to
|
||||||
test configuration or values specified in the class or module where a
|
test configuration or values specified in the class or module where a
|
||||||
test function is defined.
|
test function is defined.
|
||||||
|
@ -891,19 +885,19 @@ class Metafunc:
|
||||||
) -> None:
|
) -> None:
|
||||||
self.definition = definition
|
self.definition = definition
|
||||||
|
|
||||||
#: access to the :class:`_pytest.config.Config` object for the test session
|
#: Access to the :class:`_pytest.config.Config` object for the test session.
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
#: the module object where the test function is defined in.
|
#: The module object where the test function is defined in.
|
||||||
self.module = module
|
self.module = module
|
||||||
|
|
||||||
#: underlying python test function
|
#: Underlying Python test function.
|
||||||
self.function = definition.obj
|
self.function = definition.obj
|
||||||
|
|
||||||
#: set of fixture names required by the test function
|
#: Set of fixture names required by the test function.
|
||||||
self.fixturenames = fixtureinfo.names_closure
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
|
|
||||||
#: class object where the test function is defined in or ``None``.
|
#: Class object where the test function is defined in or ``None``.
|
||||||
self.cls = cls
|
self.cls = cls
|
||||||
|
|
||||||
self._calls = [] # type: List[CallSpec2]
|
self._calls = [] # type: List[CallSpec2]
|
||||||
|
@ -911,7 +905,7 @@ class Metafunc:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def funcargnames(self) -> List[str]:
|
def funcargnames(self) -> List[str]:
|
||||||
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
"""Alias attribute for ``fixturenames`` for pre-2.3 compatibility."""
|
||||||
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
||||||
return self.fixturenames
|
return self.fixturenames
|
||||||
|
|
||||||
|
@ -930,30 +924,35 @@ class Metafunc:
|
||||||
*,
|
*,
|
||||||
_param_mark: Optional[Mark] = None
|
_param_mark: Optional[Mark] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
""" Add new invocations to the underlying test function using the list
|
"""Add new invocations to the underlying test function using the list
|
||||||
of argvalues for the given argnames. Parametrization is performed
|
of argvalues for the given argnames. Parametrization is performed
|
||||||
during the collection phase. If you need to setup expensive resources
|
during the collection phase. If you need to setup expensive resources
|
||||||
see about setting indirect to do it rather at test setup time.
|
see about setting indirect to do it rather at test setup time.
|
||||||
|
|
||||||
:arg argnames: a comma-separated string denoting one or more argument
|
:param argnames:
|
||||||
names, or a list/tuple of argument strings.
|
A comma-separated string denoting one or more argument names, or
|
||||||
|
a list/tuple of argument strings.
|
||||||
|
|
||||||
:arg argvalues: The list of argvalues determines how often a
|
:param argvalues:
|
||||||
test is invoked with different argument values. If only one
|
The list of argvalues determines how often a test is invoked with
|
||||||
argname was specified argvalues is a list of values. If N
|
different argument values.
|
||||||
argnames were specified, argvalues must be a list of N-tuples,
|
|
||||||
where each tuple-element specifies a value for its respective
|
|
||||||
argname.
|
|
||||||
|
|
||||||
:arg indirect: The list of argnames or boolean. A list of arguments'
|
If only one argname was specified argvalues is a list of values.
|
||||||
names (subset of argnames). If True the list contains all names from
|
If N argnames were specified, argvalues must be a list of
|
||||||
the argnames. Each argvalue corresponding to an argname in this list will
|
N-tuples, where each tuple-element specifies a value for its
|
||||||
|
respective argname.
|
||||||
|
|
||||||
|
:param indirect:
|
||||||
|
A list of arguments' names (subset of argnames) or a boolean.
|
||||||
|
If True the list contains all names from the argnames. Each
|
||||||
|
argvalue corresponding to an argname in this list will
|
||||||
be passed as request.param to its respective argname fixture
|
be passed as request.param to its respective argname fixture
|
||||||
function so that it can perform more expensive setups during the
|
function so that it can perform more expensive setups during the
|
||||||
setup phase of a test rather than at collection time.
|
setup phase of a test rather than at collection time.
|
||||||
|
|
||||||
:arg ids: sequence of (or generator for) ids for ``argvalues``,
|
:param ids:
|
||||||
or a callable to return part of the id for each argvalue.
|
Sequence of (or generator for) ids for ``argvalues``,
|
||||||
|
or a callable to return part of the id for each argvalue.
|
||||||
|
|
||||||
With sequences (and generators like ``itertools.count()``) the
|
With sequences (and generators like ``itertools.count()``) the
|
||||||
returned ids should be of type ``string``, ``int``, ``float``,
|
returned ids should be of type ``string``, ``int``, ``float``,
|
||||||
|
@ -971,7 +970,8 @@ class Metafunc:
|
||||||
If no ids are provided they will be generated automatically from
|
If no ids are provided they will be generated automatically from
|
||||||
the argvalues.
|
the argvalues.
|
||||||
|
|
||||||
:arg scope: if specified it denotes the scope of the parameters.
|
:param scope:
|
||||||
|
If specified it denotes the scope of the parameters.
|
||||||
The scope is used for grouping tests by parameter instances.
|
The scope is used for grouping tests by parameter instances.
|
||||||
It will also override any fixture-function defined scope, allowing
|
It will also override any fixture-function defined scope, allowing
|
||||||
to set a dynamic scope using test context or configuration.
|
to set a dynamic scope using test context or configuration.
|
||||||
|
@ -1018,9 +1018,9 @@ class Metafunc:
|
||||||
scope, descr="parametrize() call in {}".format(self.function.__name__)
|
scope, descr="parametrize() call in {}".format(self.function.__name__)
|
||||||
)
|
)
|
||||||
|
|
||||||
# create the new calls: if we are parametrize() multiple times (by applying the decorator
|
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||||
# more than once) then we accumulate those calls generating the cartesian product
|
# more than once) then we accumulate those calls generating the cartesian product
|
||||||
# of all calls
|
# of all calls.
|
||||||
newcalls = []
|
newcalls = []
|
||||||
for callspec in self._calls or [CallSpec2(self)]:
|
for callspec in self._calls or [CallSpec2(self)]:
|
||||||
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
|
for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)):
|
||||||
|
@ -1049,15 +1049,15 @@ class Metafunc:
|
||||||
parameters: typing.Sequence[ParameterSet],
|
parameters: typing.Sequence[ParameterSet],
|
||||||
nodeid: str,
|
nodeid: str,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
|
"""Resolve the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||||
to ``parametrize``.
|
to ``parametrize``.
|
||||||
|
|
||||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||||
:param ids: the ids parameter of the parametrized call (see docs).
|
:param ids: The ids parameter of the parametrized call (see docs).
|
||||||
:param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
|
:param List[ParameterSet] parameters: The list of parameter values, same size as ``argnames``.
|
||||||
:param str str: the nodeid of the item that generated this parametrized call.
|
:param str str: The nodeid of the item that generated this parametrized call.
|
||||||
:rtype: List[str]
|
:rtype: List[str]
|
||||||
:return: the list of ids for each argname given
|
:returns: The list of ids for each argname given.
|
||||||
"""
|
"""
|
||||||
if ids is None:
|
if ids is None:
|
||||||
idfn = None
|
idfn = None
|
||||||
|
@ -1109,11 +1109,12 @@ class Metafunc:
|
||||||
argnames: typing.Sequence[str],
|
argnames: typing.Sequence[str],
|
||||||
indirect: Union[bool, typing.Sequence[str]],
|
indirect: Union[bool, typing.Sequence[str]],
|
||||||
) -> Dict[str, "Literal['params', 'funcargs']"]:
|
) -> Dict[str, "Literal['params', 'funcargs']"]:
|
||||||
"""Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg"
|
"""Resolve if each parametrized argument must be considered a
|
||||||
to the function, based on the ``indirect`` parameter of the parametrized() call.
|
parameter to a fixture or a "funcarg" to the function, based on the
|
||||||
|
``indirect`` parameter of the parametrized() call.
|
||||||
|
|
||||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||||
:param indirect: same ``indirect`` parameter of ``parametrize()``.
|
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
|
||||||
:rtype: Dict[str, str]
|
:rtype: Dict[str, str]
|
||||||
A dict mapping each arg name to either:
|
A dict mapping each arg name to either:
|
||||||
* "params" if the argname should be the parameter of a fixture of the same name.
|
* "params" if the argname should be the parameter of a fixture of the same name.
|
||||||
|
@ -1148,12 +1149,11 @@ class Metafunc:
|
||||||
argnames: typing.Sequence[str],
|
argnames: typing.Sequence[str],
|
||||||
indirect: Union[bool, typing.Sequence[str]],
|
indirect: Union[bool, typing.Sequence[str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Check if all argnames are being used, by default values, or directly/indirectly.
|
||||||
Check if all argnames are being used, by default values, or directly/indirectly.
|
|
||||||
|
|
||||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
:param List[str] argnames: List of argument names passed to ``parametrize()``.
|
||||||
:param indirect: same ``indirect`` parameter of ``parametrize()``.
|
:param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
|
||||||
:raise ValueError: if validation fails.
|
:raises ValueError: If validation fails.
|
||||||
"""
|
"""
|
||||||
default_arg_names = set(get_default_arg_names(self.function))
|
default_arg_names = set(get_default_arg_names(self.function))
|
||||||
func_name = self.function.__name__
|
func_name = self.function.__name__
|
||||||
|
@ -1204,7 +1204,7 @@ def _find_parametrized_scope(
|
||||||
if name in argnames
|
if name in argnames
|
||||||
]
|
]
|
||||||
if used_scopes:
|
if used_scopes:
|
||||||
# Takes the most narrow scope from used fixtures
|
# Takes the most narrow scope from used fixtures.
|
||||||
for scope in reversed(fixtures.scopes):
|
for scope in reversed(fixtures.scopes):
|
||||||
if scope in used_scopes:
|
if scope in used_scopes:
|
||||||
return scope
|
return scope
|
||||||
|
@ -1259,7 +1259,7 @@ def _idval(
|
||||||
elif isinstance(val, enum.Enum):
|
elif isinstance(val, enum.Enum):
|
||||||
return str(val)
|
return str(val)
|
||||||
elif isinstance(getattr(val, "__name__", None), str):
|
elif isinstance(getattr(val, "__name__", None), str):
|
||||||
# name of a class, function, module, etc.
|
# Name of a class, function, module, etc.
|
||||||
name = getattr(val, "__name__") # type: str
|
name = getattr(val, "__name__") # type: str
|
||||||
return name
|
return name
|
||||||
return str(argname) + str(idx)
|
return str(argname) + str(idx)
|
||||||
|
@ -1306,13 +1306,13 @@ def idmaker(
|
||||||
unique_ids = set(resolved_ids)
|
unique_ids = set(resolved_ids)
|
||||||
if len(unique_ids) != len(resolved_ids):
|
if len(unique_ids) != len(resolved_ids):
|
||||||
|
|
||||||
# Record the number of occurrences of each test ID
|
# Record the number of occurrences of each test ID.
|
||||||
test_id_counts = Counter(resolved_ids)
|
test_id_counts = Counter(resolved_ids)
|
||||||
|
|
||||||
# Map the test ID to its next suffix
|
# Map the test ID to its next suffix.
|
||||||
test_id_suffixes = defaultdict(int) # type: Dict[str, int]
|
test_id_suffixes = defaultdict(int) # type: Dict[str, int]
|
||||||
|
|
||||||
# Suffix non-unique IDs to make them unique
|
# Suffix non-unique IDs to make them unique.
|
||||||
for index, test_id in enumerate(resolved_ids):
|
for index, test_id in enumerate(resolved_ids):
|
||||||
if test_id_counts[test_id] > 1:
|
if test_id_counts[test_id] > 1:
|
||||||
resolved_ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id])
|
resolved_ids[index] = "{}{}".format(test_id, test_id_suffixes[test_id])
|
||||||
|
@ -1365,12 +1365,12 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
||||||
tw.sep("-", "fixtures used by {}".format(item.name))
|
tw.sep("-", "fixtures used by {}".format(item.name))
|
||||||
# TODO: Fix this type ignore.
|
# TODO: Fix this type ignore.
|
||||||
tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined]
|
tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined]
|
||||||
# dict key not used in loop but needed for sorting
|
# dict key not used in loop but needed for sorting.
|
||||||
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
||||||
assert fixturedefs is not None
|
assert fixturedefs is not None
|
||||||
if not fixturedefs:
|
if not fixturedefs:
|
||||||
continue
|
continue
|
||||||
# last item is expected to be the one used by the test item
|
# Last item is expected to be the one used by the test item.
|
||||||
write_fixture(fixturedefs[-1])
|
write_fixture(fixturedefs[-1])
|
||||||
|
|
||||||
for session_item in session.items:
|
for session_item in session.items:
|
||||||
|
@ -1446,11 +1446,35 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||||
|
|
||||||
|
|
||||||
class Function(PyobjMixin, nodes.Item):
|
class Function(PyobjMixin, nodes.Item):
|
||||||
""" a Function Item is responsible for setting up and executing a
|
"""An Item responsible for setting up and executing a Python test function.
|
||||||
Python test function.
|
|
||||||
|
param name:
|
||||||
|
The full function name, including any decorations like those
|
||||||
|
added by parametrization (``my_func[my_param]``).
|
||||||
|
param parent:
|
||||||
|
The parent Node.
|
||||||
|
param config:
|
||||||
|
The pytest Config object.
|
||||||
|
param callspec:
|
||||||
|
If given, this is function has been parametrized and the callspec contains
|
||||||
|
meta information about the parametrization.
|
||||||
|
param callobj:
|
||||||
|
If given, the object which will be called when the Function is invoked,
|
||||||
|
otherwise the callobj will be obtained from ``parent`` using ``originalname``.
|
||||||
|
param keywords:
|
||||||
|
Keywords bound to the function object for "-k" matching.
|
||||||
|
param session:
|
||||||
|
The pytest Session object.
|
||||||
|
param fixtureinfo:
|
||||||
|
Fixture information already resolved at this fixture node..
|
||||||
|
param originalname:
|
||||||
|
The attribute name to use for accessing the underlying function object.
|
||||||
|
Defaults to ``name``. Set this if name is different from the original name,
|
||||||
|
for example when it contains decorations like those added by parametrization
|
||||||
|
(``my_func[my_param]``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# disable since functions handle it themselves
|
# Disable since functions handle it themselves.
|
||||||
_ALLOW_MARKERS = False
|
_ALLOW_MARKERS = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -1465,24 +1489,6 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
fixtureinfo: Optional[FuncFixtureInfo] = None,
|
fixtureinfo: Optional[FuncFixtureInfo] = None,
|
||||||
originalname: Optional[str] = None,
|
originalname: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
param name: the full function name, including any decorations like those
|
|
||||||
added by parametrization (``my_func[my_param]``).
|
|
||||||
param parent: the parent Node.
|
|
||||||
param config: the pytest Config object
|
|
||||||
param callspec: if given, this is function has been parametrized and the callspec contains
|
|
||||||
meta information about the parametrization.
|
|
||||||
param callobj: if given, the object which will be called when the Function is invoked,
|
|
||||||
otherwise the callobj will be obtained from ``parent`` using ``originalname``
|
|
||||||
param keywords: keywords bound to the function object for "-k" matching.
|
|
||||||
param session: the pytest Session object
|
|
||||||
param fixtureinfo: fixture information already resolved at this fixture node.
|
|
||||||
param originalname:
|
|
||||||
The attribute name to use for accessing the underlying function object.
|
|
||||||
Defaults to ``name``. Set this if name is different from the original name,
|
|
||||||
for example when it contains decorations like those added by parametrization
|
|
||||||
(``my_func[my_param]``).
|
|
||||||
"""
|
|
||||||
super().__init__(name, parent, config=config, session=session)
|
super().__init__(name, parent, config=config, session=session)
|
||||||
|
|
||||||
if callobj is not NOTSET:
|
if callobj is not NOTSET:
|
||||||
|
@ -1496,8 +1502,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
#: .. versionadded:: 3.0
|
#: .. versionadded:: 3.0
|
||||||
self.originalname = originalname or name
|
self.originalname = originalname or name
|
||||||
|
|
||||||
# note: when FunctionDefinition is introduced, we should change ``originalname``
|
# Note: when FunctionDefinition is introduced, we should change ``originalname``
|
||||||
# to a readonly property that returns FunctionDefinition.name
|
# to a readonly property that returns FunctionDefinition.name.
|
||||||
|
|
||||||
self.keywords.update(self.obj.__dict__)
|
self.keywords.update(self.obj.__dict__)
|
||||||
self.own_markers.extend(get_unpacked_marks(self.obj))
|
self.own_markers.extend(get_unpacked_marks(self.obj))
|
||||||
|
@ -1535,9 +1541,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
|
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
|
||||||
"""
|
"""The public constructor."""
|
||||||
The public constructor
|
|
||||||
"""
|
|
||||||
return super().from_parent(parent=parent, **kw)
|
return super().from_parent(parent=parent, **kw)
|
||||||
|
|
||||||
def _initrequest(self) -> None:
|
def _initrequest(self) -> None:
|
||||||
|
@ -1546,7 +1550,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def function(self):
|
def function(self):
|
||||||
"underlying python 'function' object"
|
"""Underlying python 'function' object."""
|
||||||
return getimfunc(self.obj)
|
return getimfunc(self.obj)
|
||||||
|
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
|
@ -1555,17 +1559,17 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _pyfuncitem(self):
|
def _pyfuncitem(self):
|
||||||
"(compatonly) for code expecting pytest-2.2 style request objects"
|
"""(compatonly) for code expecting pytest-2.2 style request objects."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def funcargnames(self) -> List[str]:
|
def funcargnames(self) -> List[str]:
|
||||||
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
"""Alias attribute for ``fixturenames`` for pre-2.3 compatibility."""
|
||||||
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
warnings.warn(FUNCARGNAMES, stacklevel=2)
|
||||||
return self.fixturenames
|
return self.fixturenames
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
""" execute the underlying test function. """
|
"""Execute the underlying test function."""
|
||||||
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
|
@ -1589,7 +1593,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
|
|
||||||
excinfo.traceback = ntraceback.filter()
|
excinfo.traceback = ntraceback.filter()
|
||||||
# issue364: mark all but first and last frames to
|
# issue364: mark all but first and last frames to
|
||||||
# only show a single-line message for each frame
|
# only show a single-line message for each frame.
|
||||||
if self.config.getoption("tbstyle", "auto") == "auto":
|
if self.config.getoption("tbstyle", "auto") == "auto":
|
||||||
if len(excinfo.traceback) > 2:
|
if len(excinfo.traceback) > 2:
|
||||||
for entry in excinfo.traceback[1:-1]:
|
for entry in excinfo.traceback[1:-1]:
|
||||||
|
@ -1606,10 +1610,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
|
|
||||||
|
|
||||||
class FunctionDefinition(Function):
|
class FunctionDefinition(Function):
|
||||||
"""
|
"""Internal hack until we get actual definition nodes instead of the
|
||||||
internal hack until we get actual definition nodes instead of the
|
crappy metafunc hack."""
|
||||||
crappy metafunc hack
|
|
||||||
"""
|
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
raise RuntimeError("function definitions are not supposed to be used")
|
raise RuntimeError("function definitions are not supposed to be used")
|
||||||
|
|
|
@ -39,10 +39,8 @@ def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
|
||||||
|
|
||||||
|
|
||||||
class ApproxBase:
|
class ApproxBase:
|
||||||
"""
|
"""Provide shared utilities for making approximate comparisons between
|
||||||
Provide shared utilities for making approximate comparisons between numbers
|
numbers or sequences of numbers."""
|
||||||
or sequences of numbers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Tell numpy to use our `__eq__` operator instead of its.
|
# Tell numpy to use our `__eq__` operator instead of its.
|
||||||
__array_ufunc__ = None
|
__array_ufunc__ = None
|
||||||
|
@ -74,16 +72,14 @@ class ApproxBase:
|
||||||
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
|
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
|
||||||
|
|
||||||
def _yield_comparisons(self, actual):
|
def _yield_comparisons(self, actual):
|
||||||
"""
|
"""Yield all the pairs of numbers to be compared.
|
||||||
Yield all the pairs of numbers to be compared. This is used to
|
|
||||||
implement the `__eq__` method.
|
This is used to implement the `__eq__` method.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _check_type(self) -> None:
|
def _check_type(self) -> None:
|
||||||
"""
|
"""Raise a TypeError if the expected value is not a valid type."""
|
||||||
Raise a TypeError if the expected value is not a valid type.
|
|
||||||
"""
|
|
||||||
# This is only a concern if the expected value is a sequence. In every
|
# This is only a concern if the expected value is a sequence. In every
|
||||||
# other case, the approx() function ensures that the expected value has
|
# other case, the approx() function ensures that the expected value has
|
||||||
# a numeric type. For this reason, the default is to do nothing. The
|
# a numeric type. For this reason, the default is to do nothing. The
|
||||||
|
@ -100,9 +96,7 @@ def _recursive_list_map(f, x):
|
||||||
|
|
||||||
|
|
||||||
class ApproxNumpy(ApproxBase):
|
class ApproxNumpy(ApproxBase):
|
||||||
"""
|
"""Perform approximate comparisons where the expected value is numpy array."""
|
||||||
Perform approximate comparisons where the expected value is numpy array.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
|
list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
|
||||||
|
@ -111,7 +105,7 @@ class ApproxNumpy(ApproxBase):
|
||||||
def __eq__(self, actual) -> bool:
|
def __eq__(self, actual) -> bool:
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# self.expected is supposed to always be an array here
|
# self.expected is supposed to always be an array here.
|
||||||
|
|
||||||
if not np.isscalar(actual):
|
if not np.isscalar(actual):
|
||||||
try:
|
try:
|
||||||
|
@ -142,10 +136,8 @@ class ApproxNumpy(ApproxBase):
|
||||||
|
|
||||||
|
|
||||||
class ApproxMapping(ApproxBase):
|
class ApproxMapping(ApproxBase):
|
||||||
"""
|
"""Perform approximate comparisons where the expected value is a mapping
|
||||||
Perform approximate comparisons where the expected value is a mapping with
|
with numeric values (the keys can be anything)."""
|
||||||
numeric values (the keys can be anything).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "approx({!r})".format(
|
return "approx({!r})".format(
|
||||||
|
@ -173,10 +165,7 @@ class ApproxMapping(ApproxBase):
|
||||||
|
|
||||||
|
|
||||||
class ApproxSequencelike(ApproxBase):
|
class ApproxSequencelike(ApproxBase):
|
||||||
"""
|
"""Perform approximate comparisons where the expected value is a sequence of numbers."""
|
||||||
Perform approximate comparisons where the expected value is a sequence of
|
|
||||||
numbers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
seq_type = type(self.expected)
|
seq_type = type(self.expected)
|
||||||
|
@ -207,9 +196,7 @@ class ApproxSequencelike(ApproxBase):
|
||||||
|
|
||||||
|
|
||||||
class ApproxScalar(ApproxBase):
|
class ApproxScalar(ApproxBase):
|
||||||
"""
|
"""Perform approximate comparisons where the expected value is a single number."""
|
||||||
Perform approximate comparisons where the expected value is a single number.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Using Real should be better than this Union, but not possible yet:
|
# Using Real should be better than this Union, but not possible yet:
|
||||||
# https://github.com/python/typeshed/pull/3108
|
# https://github.com/python/typeshed/pull/3108
|
||||||
|
@ -217,13 +204,14 @@ class ApproxScalar(ApproxBase):
|
||||||
DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal]
|
DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal]
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""
|
"""Return a string communicating both the expected value and the
|
||||||
Return a string communicating both the expected value and the tolerance
|
tolerance for the comparison being made.
|
||||||
for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'.
|
|
||||||
|
For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Infinities aren't compared using tolerances, so don't show a
|
# Infinities aren't compared using tolerances, so don't show a
|
||||||
# tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j)
|
# tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j).
|
||||||
if math.isinf(abs(self.expected)):
|
if math.isinf(abs(self.expected)):
|
||||||
return str(self.expected)
|
return str(self.expected)
|
||||||
|
|
||||||
|
@ -239,10 +227,8 @@ class ApproxScalar(ApproxBase):
|
||||||
return "{} ± {}".format(self.expected, vetted_tolerance)
|
return "{} ± {}".format(self.expected, vetted_tolerance)
|
||||||
|
|
||||||
def __eq__(self, actual) -> bool:
|
def __eq__(self, actual) -> bool:
|
||||||
"""
|
"""Return whether the given value is equal to the expected value
|
||||||
Return true if the given value is equal to the expected value within
|
within the pre-specified tolerance."""
|
||||||
the pre-specified tolerance.
|
|
||||||
"""
|
|
||||||
if _is_numpy_array(actual):
|
if _is_numpy_array(actual):
|
||||||
# Call ``__eq__()`` manually to prevent infinite-recursion with
|
# Call ``__eq__()`` manually to prevent infinite-recursion with
|
||||||
# numpy<1.13. See #3748.
|
# numpy<1.13. See #3748.
|
||||||
|
@ -276,10 +262,10 @@ class ApproxScalar(ApproxBase):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tolerance(self):
|
def tolerance(self):
|
||||||
"""
|
"""Return the tolerance for the comparison.
|
||||||
Return the tolerance for the comparison. This could be either an
|
|
||||||
absolute tolerance or a relative tolerance, depending on what the user
|
This could be either an absolute tolerance or a relative tolerance,
|
||||||
specified or which would be larger.
|
depending on what the user specified or which would be larger.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_default(x, default):
|
def set_default(x, default):
|
||||||
|
@ -323,17 +309,14 @@ class ApproxScalar(ApproxBase):
|
||||||
|
|
||||||
|
|
||||||
class ApproxDecimal(ApproxScalar):
|
class ApproxDecimal(ApproxScalar):
|
||||||
"""
|
"""Perform approximate comparisons where the expected value is a Decimal."""
|
||||||
Perform approximate comparisons where the expected value is a decimal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
|
DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
|
||||||
DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
|
DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
|
||||||
|
|
||||||
|
|
||||||
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||||
"""
|
"""Assert that two numbers (or two sets of numbers) are equal to each other
|
||||||
Assert that two numbers (or two sets of numbers) are equal to each other
|
|
||||||
within some tolerance.
|
within some tolerance.
|
||||||
|
|
||||||
Due to the `intricacies of floating-point arithmetic`__, numbers that we
|
Due to the `intricacies of floating-point arithmetic`__, numbers that we
|
||||||
|
@ -522,9 +505,9 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||||
|
|
||||||
|
|
||||||
def _is_numpy_array(obj: object) -> bool:
|
def _is_numpy_array(obj: object) -> bool:
|
||||||
"""
|
"""Return true if the given object is a numpy array.
|
||||||
Return true if the given object is a numpy array. Make a special effort to
|
|
||||||
avoid importing numpy unless it's really necessary.
|
A special effort is made to avoid importing numpy unless it's really necessary.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -563,11 +546,11 @@ def raises( # noqa: F811
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any
|
**kwargs: Any
|
||||||
) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]:
|
) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]:
|
||||||
r"""
|
r"""Assert that a code block/function call raises ``expected_exception``
|
||||||
Assert that a code block/function call raises ``expected_exception``
|
|
||||||
or raise a failure exception otherwise.
|
or raise a failure exception otherwise.
|
||||||
|
|
||||||
:kwparam match: if specified, a string containing a regular expression,
|
:kwparam match:
|
||||||
|
If specified, a string containing a regular expression,
|
||||||
or a regular expression object, that is tested against the string
|
or a regular expression object, that is tested against the string
|
||||||
representation of the exception using ``re.search``. To match a literal
|
representation of the exception using ``re.search``. To match a literal
|
||||||
string that may contain `special characters`__, the pattern can
|
string that may contain `special characters`__, the pattern can
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" recording warnings during test function execution. """
|
"""Record warnings during test function execution."""
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
|
|
|
@ -94,9 +94,8 @@ class BaseReport:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def longreprtext(self) -> str:
|
def longreprtext(self) -> str:
|
||||||
"""
|
"""Read-only property that returns the full string representation of
|
||||||
Read-only property that returns the full string representation
|
``longrepr``.
|
||||||
of ``longrepr``.
|
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
.. versionadded:: 3.0
|
||||||
"""
|
"""
|
||||||
|
@ -109,7 +108,7 @@ class BaseReport:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def caplog(self) -> str:
|
def caplog(self) -> str:
|
||||||
"""Return captured log lines, if log capturing is enabled
|
"""Return captured log lines, if log capturing is enabled.
|
||||||
|
|
||||||
.. versionadded:: 3.5
|
.. versionadded:: 3.5
|
||||||
"""
|
"""
|
||||||
|
@ -119,7 +118,7 @@ class BaseReport:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capstdout(self) -> str:
|
def capstdout(self) -> str:
|
||||||
"""Return captured text from stdout, if capturing is enabled
|
"""Return captured text from stdout, if capturing is enabled.
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
.. versionadded:: 3.0
|
||||||
"""
|
"""
|
||||||
|
@ -129,7 +128,7 @@ class BaseReport:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capstderr(self) -> str:
|
def capstderr(self) -> str:
|
||||||
"""Return captured text from stderr, if capturing is enabled
|
"""Return captured text from stderr, if capturing is enabled.
|
||||||
|
|
||||||
.. versionadded:: 3.0
|
.. versionadded:: 3.0
|
||||||
"""
|
"""
|
||||||
|
@ -147,11 +146,8 @@ class BaseReport:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def count_towards_summary(self) -> bool:
|
def count_towards_summary(self) -> bool:
|
||||||
"""
|
"""**Experimental** Whether this report should be counted towards the
|
||||||
**Experimental**
|
totals shown at the end of the test session: "1 passed, 1 failure, etc".
|
||||||
|
|
||||||
``True`` if this report should be counted towards the totals shown at the end of the
|
|
||||||
test session: "1 passed, 1 failure, etc".
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -162,11 +158,9 @@ class BaseReport:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def head_line(self) -> Optional[str]:
|
def head_line(self) -> Optional[str]:
|
||||||
"""
|
"""**Experimental** The head line shown with longrepr output for this
|
||||||
**Experimental**
|
report, more commonly during traceback representation during
|
||||||
|
failures::
|
||||||
Returns the head line shown with longrepr output for this report, more commonly during
|
|
||||||
traceback representation during failures::
|
|
||||||
|
|
||||||
________ Test.foo ________
|
________ Test.foo ________
|
||||||
|
|
||||||
|
@ -190,11 +184,10 @@ class BaseReport:
|
||||||
return verbose
|
return verbose
|
||||||
|
|
||||||
def _to_json(self) -> Dict[str, Any]:
|
def _to_json(self) -> Dict[str, Any]:
|
||||||
"""
|
"""Return the contents of this report as a dict of builtin entries,
|
||||||
This was originally the serialize_report() function from xdist (ca03269).
|
suitable for serialization.
|
||||||
|
|
||||||
Returns the contents of this report as a dict of builtin entries, suitable for
|
This was originally the serialize_report() function from xdist (ca03269).
|
||||||
serialization.
|
|
||||||
|
|
||||||
Experimental method.
|
Experimental method.
|
||||||
"""
|
"""
|
||||||
|
@ -202,11 +195,11 @@ class BaseReport:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R:
|
def _from_json(cls: "Type[_R]", reportdict: Dict[str, object]) -> _R:
|
||||||
"""
|
"""Create either a TestReport or CollectReport, depending on the calling class.
|
||||||
This was originally the serialize_report() function from xdist (ca03269).
|
|
||||||
|
|
||||||
Factory method that returns either a TestReport or CollectReport, depending on the calling
|
It is the callers responsibility to know which class to pass here.
|
||||||
class. It's the callers responsibility to know which class to pass here.
|
|
||||||
|
This was originally the serialize_report() function from xdist (ca03269).
|
||||||
|
|
||||||
Experimental method.
|
Experimental method.
|
||||||
"""
|
"""
|
||||||
|
@ -229,9 +222,8 @@ def _report_unserialization_failure(
|
||||||
|
|
||||||
|
|
||||||
class TestReport(BaseReport):
|
class TestReport(BaseReport):
|
||||||
""" Basic test report object (also used for setup and teardown calls if
|
"""Basic test report object (also used for setup and teardown calls if
|
||||||
they fail).
|
they fail)."""
|
||||||
"""
|
|
||||||
|
|
||||||
__test__ = False
|
__test__ = False
|
||||||
|
|
||||||
|
@ -248,38 +240,38 @@ class TestReport(BaseReport):
|
||||||
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
|
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
|
||||||
**extra
|
**extra
|
||||||
) -> None:
|
) -> None:
|
||||||
#: normalized collection node id
|
#: Normalized collection nodeid.
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
|
|
||||||
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
#: A (filesystempath, lineno, domaininfo) tuple indicating the
|
||||||
#: actual location of a test item - it might be different from the
|
#: actual location of a test item - it might be different from the
|
||||||
#: collected one e.g. if a method is inherited from a different module.
|
#: collected one e.g. if a method is inherited from a different module.
|
||||||
self.location = location # type: Tuple[str, Optional[int], str]
|
self.location = location # type: Tuple[str, Optional[int], str]
|
||||||
|
|
||||||
#: a name -> value dictionary containing all keywords and
|
#: A name -> value dictionary containing all keywords and
|
||||||
#: markers associated with a test invocation.
|
#: markers associated with a test invocation.
|
||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
|
|
||||||
#: test outcome, always one of "passed", "failed", "skipped".
|
#: Test outcome, always one of "passed", "failed", "skipped".
|
||||||
self.outcome = outcome
|
self.outcome = outcome
|
||||||
|
|
||||||
#: None or a failure representation.
|
#: None or a failure representation.
|
||||||
self.longrepr = longrepr
|
self.longrepr = longrepr
|
||||||
|
|
||||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
#: One of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||||
self.when = when
|
self.when = when
|
||||||
|
|
||||||
#: user properties is a list of tuples (name, value) that holds user
|
#: User properties is a list of tuples (name, value) that holds user
|
||||||
#: defined properties of the test
|
#: defined properties of the test.
|
||||||
self.user_properties = list(user_properties or [])
|
self.user_properties = list(user_properties or [])
|
||||||
|
|
||||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
#: List of pairs ``(str, str)`` of extra information which needs to
|
||||||
#: marshallable. Used by pytest to add captured text
|
#: marshallable. Used by pytest to add captured text
|
||||||
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
||||||
#: to add arbitrary information to reports.
|
#: to add arbitrary information to reports.
|
||||||
self.sections = list(sections)
|
self.sections = list(sections)
|
||||||
|
|
||||||
#: time it took to run just the test
|
#: Time it took to run just the test.
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
|
|
||||||
self.__dict__.update(extra)
|
self.__dict__.update(extra)
|
||||||
|
@ -291,9 +283,7 @@ class TestReport(BaseReport):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
|
def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
|
||||||
"""
|
"""Create and fill a TestReport with standard item and call info."""
|
||||||
Factory method to create and fill a TestReport with standard item and call info.
|
|
||||||
"""
|
|
||||||
when = call.when
|
when = call.when
|
||||||
# Remove "collect" from the Literal type -- only for collection calls.
|
# Remove "collect" from the Literal type -- only for collection calls.
|
||||||
assert when != "collect"
|
assert when != "collect"
|
||||||
|
@ -350,10 +340,10 @@ class CollectReport(BaseReport):
|
||||||
sections: Iterable[Tuple[str, str]] = (),
|
sections: Iterable[Tuple[str, str]] = (),
|
||||||
**extra
|
**extra
|
||||||
) -> None:
|
) -> None:
|
||||||
#: normalized collection node id
|
#: Normalized collection nodeid.
|
||||||
self.nodeid = nodeid
|
self.nodeid = nodeid
|
||||||
|
|
||||||
#: test outcome, always one of "passed", "failed", "skipped".
|
#: Test outcome, always one of "passed", "failed", "skipped".
|
||||||
self.outcome = outcome
|
self.outcome = outcome
|
||||||
|
|
||||||
#: None or a failure representation.
|
#: None or a failure representation.
|
||||||
|
@ -362,10 +352,11 @@ class CollectReport(BaseReport):
|
||||||
#: The collected items and collection nodes.
|
#: The collected items and collection nodes.
|
||||||
self.result = result or []
|
self.result = result or []
|
||||||
|
|
||||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
#: List of pairs ``(str, str)`` of extra information which needs to
|
||||||
#: marshallable. Used by pytest to add captured text
|
#: marshallable.
|
||||||
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
# Used by pytest to add captured text : from ``stdout`` and ``stderr``,
|
||||||
#: to add arbitrary information to reports.
|
# but may be used by other plugins : to add arbitrary information to
|
||||||
|
# reports.
|
||||||
self.sections = list(sections)
|
self.sections = list(sections)
|
||||||
|
|
||||||
self.__dict__.update(extra)
|
self.__dict__.update(extra)
|
||||||
|
@ -413,11 +404,10 @@ def pytest_report_from_serializable(
|
||||||
|
|
||||||
|
|
||||||
def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
||||||
"""
|
"""Return the contents of this report as a dict of builtin entries,
|
||||||
This was originally the serialize_report() function from xdist (ca03269).
|
suitable for serialization.
|
||||||
|
|
||||||
Returns the contents of this report as a dict of builtin entries, suitable for
|
This was originally the serialize_report() function from xdist (ca03269).
|
||||||
serialization.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def serialize_repr_entry(
|
def serialize_repr_entry(
|
||||||
|
@ -485,10 +475,10 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""Return **kwargs that can be used to construct a TestReport or
|
||||||
This was originally the serialize_report() function from xdist (ca03269).
|
CollectReport instance.
|
||||||
|
|
||||||
Returns **kwargs that can be used to construct a TestReport or CollectReport instance.
|
This was originally the serialize_report() function from xdist (ca03269).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def deserialize_repr_entry(entry_data):
|
def deserialize_repr_entry(entry_data):
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
""" log machine-parseable test session result information in a plain
|
"""log machine-parseable test session result information to a plain text file."""
|
||||||
text file.
|
|
||||||
"""
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
@ -30,7 +28,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
|
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
resultlog = config.option.resultlog
|
resultlog = config.option.resultlog
|
||||||
# prevent opening resultlog on worker nodes (xdist)
|
# Prevent opening resultlog on worker nodes (xdist).
|
||||||
if resultlog and not hasattr(config, "workerinput"):
|
if resultlog and not hasattr(config, "workerinput"):
|
||||||
dirname = os.path.dirname(os.path.abspath(resultlog))
|
dirname = os.path.dirname(os.path.abspath(resultlog))
|
||||||
if not os.path.isdir(dirname):
|
if not os.path.isdir(dirname):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" basic collect and runtest protocol implementations """
|
"""Basic collect and runtest protocol implementations."""
|
||||||
import bdb
|
import bdb
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
@ -39,7 +39,7 @@ if TYPE_CHECKING:
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
#
|
#
|
||||||
# pytest plugin hooks
|
# pytest plugin hooks.
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
|
@ -116,8 +116,8 @@ def runtestprotocol(
|
||||||
if not item.config.getoption("setuponly", False):
|
if not item.config.getoption("setuponly", False):
|
||||||
reports.append(call_and_report(item, "call", log))
|
reports.append(call_and_report(item, "call", log))
|
||||||
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
|
reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
|
||||||
# after all teardown hooks have been called
|
# After all teardown hooks have been called
|
||||||
# want funcargs and request info to go away
|
# want funcargs and request info to go away.
|
||||||
if hasrequest:
|
if hasrequest:
|
||||||
item._request = False # type: ignore[attr-defined]
|
item._request = False # type: ignore[attr-defined]
|
||||||
item.funcargs = None # type: ignore[attr-defined]
|
item.funcargs = None # type: ignore[attr-defined]
|
||||||
|
@ -170,8 +170,7 @@ def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
|
||||||
def _update_current_test_var(
|
def _update_current_test_var(
|
||||||
item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
|
item: Item, when: Optional["Literal['setup', 'call', 'teardown']"]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
|
||||||
Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
|
|
||||||
|
|
||||||
If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
|
If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
|
||||||
"""
|
"""
|
||||||
|
@ -253,15 +252,21 @@ _T = TypeVar("_T")
|
||||||
|
|
||||||
@attr.s(repr=False)
|
@attr.s(repr=False)
|
||||||
class CallInfo(Generic[_T]):
|
class CallInfo(Generic[_T]):
|
||||||
""" Result/Exception info a function invocation.
|
"""Result/Exception info a function invocation.
|
||||||
|
|
||||||
:param T result: The return value of the call, if it didn't raise. Can only be accessed
|
:param T result:
|
||||||
if excinfo is None.
|
The return value of the call, if it didn't raise. Can only be
|
||||||
:param Optional[ExceptionInfo] excinfo: The captured exception of the call, if it raised.
|
accessed if excinfo is None.
|
||||||
:param float start: The system time when the call started, in seconds since the epoch.
|
:param Optional[ExceptionInfo] excinfo:
|
||||||
:param float stop: The system time when the call ended, in seconds since the epoch.
|
The captured exception of the call, if it raised.
|
||||||
:param float duration: The call duration, in seconds.
|
:param float start:
|
||||||
:param str when: The context of invocation: "setup", "call", "teardown", ...
|
The system time when the call started, in seconds since the epoch.
|
||||||
|
:param float stop:
|
||||||
|
The system time when the call ended, in seconds since the epoch.
|
||||||
|
:param float duration:
|
||||||
|
The call duration, in seconds.
|
||||||
|
:param str when:
|
||||||
|
The context of invocation: "setup", "call", "teardown", ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_result = attr.ib(type="Optional[_T]")
|
_result = attr.ib(type="Optional[_T]")
|
||||||
|
@ -352,14 +357,14 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||||
|
|
||||||
|
|
||||||
class SetupState:
|
class SetupState:
|
||||||
""" shared state for setting up/tearing down test items or collectors. """
|
"""Shared state for setting up/tearing down test items or collectors."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.stack = [] # type: List[Node]
|
self.stack = [] # type: List[Node]
|
||||||
self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]]
|
self._finalizers = {} # type: Dict[Node, List[Callable[[], object]]]
|
||||||
|
|
||||||
def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None:
|
def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None:
|
||||||
""" attach a finalizer to the given colitem. """
|
"""Attach a finalizer to the given colitem."""
|
||||||
assert colitem and not isinstance(colitem, tuple)
|
assert colitem and not isinstance(colitem, tuple)
|
||||||
assert callable(finalizer)
|
assert callable(finalizer)
|
||||||
# assert colitem in self.stack # some unit tests don't setup stack :/
|
# assert colitem in self.stack # some unit tests don't setup stack :/
|
||||||
|
@ -419,7 +424,7 @@ class SetupState:
|
||||||
def prepare(self, colitem) -> None:
|
def prepare(self, colitem) -> None:
|
||||||
"""Setup objects along the collector chain to the test-method."""
|
"""Setup objects along the collector chain to the test-method."""
|
||||||
|
|
||||||
# check if the last collection node has raised an error
|
# Check if the last collection node has raised an error.
|
||||||
for col in self.stack:
|
for col in self.stack:
|
||||||
if hasattr(col, "_prepare_exc"):
|
if hasattr(col, "_prepare_exc"):
|
||||||
exc = col._prepare_exc # type: ignore[attr-defined]
|
exc = col._prepare_exc # type: ignore[attr-defined]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" support for skip/xfail functions and markers. """
|
"""Support for skip/xfail functions and markers."""
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
@ -298,9 +298,9 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]):
|
||||||
and rep.skipped
|
and rep.skipped
|
||||||
and type(rep.longrepr) is tuple
|
and type(rep.longrepr) is tuple
|
||||||
):
|
):
|
||||||
# skipped by mark.skipif; change the location of the failure
|
# Skipped by mark.skipif; change the location of the failure
|
||||||
# to point to the item definition, otherwise it will display
|
# to point to the item definition, otherwise it will display
|
||||||
# the location of where the skip exception was raised within pytest
|
# the location of where the skip exception was raised within pytest.
|
||||||
_, _, reason = rep.longrepr
|
_, _, reason = rep.longrepr
|
||||||
filename, line = item.reportinfo()[:2]
|
filename, line = item.reportinfo()[:2]
|
||||||
assert line is not None
|
assert line is not None
|
||||||
|
|
|
@ -92,7 +92,7 @@ class Store:
|
||||||
def __getitem__(self, key: StoreKey[T]) -> T:
|
def __getitem__(self, key: StoreKey[T]) -> T:
|
||||||
"""Get the value for key.
|
"""Get the value for key.
|
||||||
|
|
||||||
Raises KeyError if the key wasn't set before.
|
Raises ``KeyError`` if the key wasn't set before.
|
||||||
"""
|
"""
|
||||||
return cast(T, self._store[key])
|
return cast(T, self._store[key])
|
||||||
|
|
||||||
|
@ -116,10 +116,10 @@ class Store:
|
||||||
def __delitem__(self, key: StoreKey[T]) -> None:
|
def __delitem__(self, key: StoreKey[T]) -> None:
|
||||||
"""Delete the value for key.
|
"""Delete the value for key.
|
||||||
|
|
||||||
Raises KeyError if the key wasn't set before.
|
Raises ``KeyError`` if the key wasn't set before.
|
||||||
"""
|
"""
|
||||||
del self._store[key]
|
del self._store[key]
|
||||||
|
|
||||||
def __contains__(self, key: StoreKey[T]) -> bool:
|
def __contains__(self, key: StoreKey[T]) -> bool:
|
||||||
"""Returns whether key was set."""
|
"""Return whether key was set."""
|
||||||
return key in self._store
|
return key in self._store
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" terminal reporting of the full testing process.
|
"""Terminal reporting of the full testing process.
|
||||||
|
|
||||||
This is a good source for looking at the various reporting hooks.
|
This is a good source for looking at the various reporting hooks.
|
||||||
"""
|
"""
|
||||||
|
@ -69,11 +69,10 @@ _REPORTCHARS_DEFAULT = "fE"
|
||||||
|
|
||||||
|
|
||||||
class MoreQuietAction(argparse.Action):
|
class MoreQuietAction(argparse.Action):
|
||||||
"""
|
"""A modified copy of the argparse count action which counts down and updates
|
||||||
a modified copy of the argparse count action which counts down and updates
|
the legacy quiet attribute at the same time.
|
||||||
the legacy quiet attribute at the same time
|
|
||||||
|
|
||||||
used to unify verbosity handling
|
Used to unify verbosity handling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -276,13 +275,14 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class WarningReport:
|
class WarningReport:
|
||||||
"""
|
"""Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
|
||||||
Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
|
|
||||||
|
|
||||||
:ivar str message: user friendly message about the warning
|
:ivar str message:
|
||||||
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
|
User friendly message about the warning.
|
||||||
|
:ivar str|None nodeid:
|
||||||
|
nodeid that generated the warning (see ``get_location``).
|
||||||
:ivar tuple|py.path.local fslocation:
|
:ivar tuple|py.path.local fslocation:
|
||||||
file system location of the source of the warning (see ``get_location``).
|
File system location of the source of the warning (see ``get_location``).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
message = attr.ib(type=str)
|
message = attr.ib(type=str)
|
||||||
|
@ -293,10 +293,7 @@ class WarningReport:
|
||||||
count_towards_summary = True
|
count_towards_summary = True
|
||||||
|
|
||||||
def get_location(self, config: Config) -> Optional[str]:
|
def get_location(self, config: Config) -> Optional[str]:
|
||||||
"""
|
"""Return the more user-friendly information about the location of a warning, or None."""
|
||||||
Returns the more user-friendly information about the location
|
|
||||||
of a warning, or None.
|
|
||||||
"""
|
|
||||||
if self.nodeid:
|
if self.nodeid:
|
||||||
return self.nodeid
|
return self.nodeid
|
||||||
if self.fslocation:
|
if self.fslocation:
|
||||||
|
@ -349,7 +346,7 @@ class TerminalReporter:
|
||||||
self._tw = value
|
self._tw = value
|
||||||
|
|
||||||
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
|
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
|
||||||
"""Return True if we should display progress information based on the current config"""
|
"""Return whether we should display progress information based on the current config."""
|
||||||
# do not show progress if we are not capturing output (#3038)
|
# do not show progress if we are not capturing output (#3038)
|
||||||
if self.config.getoption("capture", "no") == "no":
|
if self.config.getoption("capture", "no") == "no":
|
||||||
return False
|
return False
|
||||||
|
@ -439,10 +436,10 @@ class TerminalReporter:
|
||||||
self._tw.line(line, **markup)
|
self._tw.line(line, **markup)
|
||||||
|
|
||||||
def rewrite(self, line: str, **markup: bool) -> None:
|
def rewrite(self, line: str, **markup: bool) -> None:
|
||||||
"""
|
"""Rewinds the terminal cursor to the beginning and writes the given line.
|
||||||
Rewinds the terminal cursor to the beginning and writes the given line.
|
|
||||||
|
|
||||||
:kwarg erase: if True, will also add spaces until the full terminal width to ensure
|
:param erase:
|
||||||
|
If True, will also add spaces until the full terminal width to ensure
|
||||||
previous lines are properly erased.
|
previous lines are properly erased.
|
||||||
|
|
||||||
The rest of the keyword arguments are markup instructions.
|
The rest of the keyword arguments are markup instructions.
|
||||||
|
@ -499,9 +496,9 @@ class TerminalReporter:
|
||||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
||||||
if self.config.option.traceconfig:
|
if self.config.option.traceconfig:
|
||||||
msg = "PLUGIN registered: {}".format(plugin)
|
msg = "PLUGIN registered: {}".format(plugin)
|
||||||
# XXX this event may happen during setup/teardown time
|
# XXX This event may happen during setup/teardown time
|
||||||
# which unfortunately captures our output here
|
# which unfortunately captures our output here
|
||||||
# which garbles our output if we use self.write_line
|
# which garbles our output if we use self.write_line.
|
||||||
self.write_line(msg)
|
self.write_line(msg)
|
||||||
|
|
||||||
def pytest_deselected(self, items: Sequence[Item]) -> None:
|
def pytest_deselected(self, items: Sequence[Item]) -> None:
|
||||||
|
@ -510,8 +507,8 @@ class TerminalReporter:
|
||||||
def pytest_runtest_logstart(
|
def pytest_runtest_logstart(
|
||||||
self, nodeid: str, location: Tuple[str, Optional[int], str]
|
self, nodeid: str, location: Tuple[str, Optional[int], str]
|
||||||
) -> None:
|
) -> None:
|
||||||
# ensure that the path is printed before the
|
# Ensure that the path is printed before the
|
||||||
# 1st test of a module starts running
|
# 1st test of a module starts running.
|
||||||
if self.showlongtestinfo:
|
if self.showlongtestinfo:
|
||||||
line = self._locationline(nodeid, *location)
|
line = self._locationline(nodeid, *location)
|
||||||
self.write_ensure_prefix(line, "")
|
self.write_ensure_prefix(line, "")
|
||||||
|
@ -533,7 +530,7 @@ class TerminalReporter:
|
||||||
markup = None
|
markup = None
|
||||||
self._add_stats(category, [rep])
|
self._add_stats(category, [rep])
|
||||||
if not letter and not word:
|
if not letter and not word:
|
||||||
# probably passed setup/teardown
|
# Probably passed setup/teardown.
|
||||||
return
|
return
|
||||||
running_xdist = hasattr(rep, "node")
|
running_xdist = hasattr(rep, "node")
|
||||||
if markup is None:
|
if markup is None:
|
||||||
|
@ -623,7 +620,7 @@ class TerminalReporter:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _width_of_current_line(self) -> int:
|
def _width_of_current_line(self) -> int:
|
||||||
"""Return the width of current line, using the superior implementation of py-1.6 when available"""
|
"""Return the width of the current line."""
|
||||||
return self._tw.width_of_current_line
|
return self._tw.width_of_current_line
|
||||||
|
|
||||||
def pytest_collection(self) -> None:
|
def pytest_collection(self) -> None:
|
||||||
|
@ -761,9 +758,9 @@ class TerminalReporter:
|
||||||
rep.toterminal(self._tw)
|
rep.toterminal(self._tw)
|
||||||
|
|
||||||
def _printcollecteditems(self, items: Sequence[Item]) -> None:
|
def _printcollecteditems(self, items: Sequence[Item]) -> None:
|
||||||
# to print out items and their parent collectors
|
# To print out items and their parent collectors
|
||||||
# we take care to leave out Instances aka ()
|
# we take care to leave out Instances aka ()
|
||||||
# because later versions are going to get rid of them anyway
|
# because later versions are going to get rid of them anyway.
|
||||||
if self.config.option.verbose < 0:
|
if self.config.option.verbose < 0:
|
||||||
if self.config.option.verbose < -1:
|
if self.config.option.verbose < -1:
|
||||||
counts = {} # type: Dict[str, int]
|
counts = {} # type: Dict[str, int]
|
||||||
|
@ -868,7 +865,7 @@ class TerminalReporter:
|
||||||
line += "[".join(values)
|
line += "[".join(values)
|
||||||
return line
|
return line
|
||||||
|
|
||||||
# collect_fspath comes from testid which has a "/"-normalized path
|
# collect_fspath comes from testid which has a "/"-normalized path.
|
||||||
|
|
||||||
if fspath:
|
if fspath:
|
||||||
res = mkrel(nodeid)
|
res = mkrel(nodeid)
|
||||||
|
@ -896,7 +893,7 @@ class TerminalReporter:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
#
|
#
|
||||||
# summaries for sessionfinish
|
# Summaries for sessionfinish.
|
||||||
#
|
#
|
||||||
def getreports(self, name: str):
|
def getreports(self, name: str):
|
||||||
values = []
|
values = []
|
||||||
|
@ -1255,9 +1252,9 @@ def _folded_skips(
|
||||||
# For consistency, report all fspaths in relative form.
|
# For consistency, report all fspaths in relative form.
|
||||||
fspath = startdir.bestrelpath(py.path.local(fspath))
|
fspath = startdir.bestrelpath(py.path.local(fspath))
|
||||||
keywords = getattr(event, "keywords", {})
|
keywords = getattr(event, "keywords", {})
|
||||||
# folding reports with global pytestmark variable
|
# Folding reports with global pytestmark variable.
|
||||||
# this is workaround, because for now we cannot identify the scope of a skip marker
|
# This is a workaround, because for now we cannot identify the scope of a skip marker
|
||||||
# TODO: revisit after marks scope would be fixed
|
# TODO: Revisit after marks scope would be fixed.
|
||||||
if (
|
if (
|
||||||
event.when == "setup"
|
event.when == "setup"
|
||||||
and "skip" in keywords
|
and "skip" in keywords
|
||||||
|
@ -1298,20 +1295,19 @@ def _make_plural(count: int, noun: str) -> Tuple[int, str]:
|
||||||
def _plugin_nameversions(plugininfo) -> List[str]:
|
def _plugin_nameversions(plugininfo) -> List[str]:
|
||||||
values = [] # type: List[str]
|
values = [] # type: List[str]
|
||||||
for plugin, dist in plugininfo:
|
for plugin, dist in plugininfo:
|
||||||
# gets us name and version!
|
# Gets us name and version!
|
||||||
name = "{dist.project_name}-{dist.version}".format(dist=dist)
|
name = "{dist.project_name}-{dist.version}".format(dist=dist)
|
||||||
# questionable convenience, but it keeps things short
|
# Questionable convenience, but it keeps things short.
|
||||||
if name.startswith("pytest-"):
|
if name.startswith("pytest-"):
|
||||||
name = name[7:]
|
name = name[7:]
|
||||||
# we decided to print python package names
|
# We decided to print python package names they can have more than one plugin.
|
||||||
# they can have more than one plugin
|
|
||||||
if name not in values:
|
if name not in values:
|
||||||
values.append(name)
|
values.append(name)
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def format_session_duration(seconds: float) -> str:
|
def format_session_duration(seconds: float) -> str:
|
||||||
"""Format the given seconds in a human readable manner to show in the final summary"""
|
"""Format the given seconds in a human readable manner to show in the final summary."""
|
||||||
if seconds < 60:
|
if seconds < 60:
|
||||||
return "{:.2f}s".format(seconds)
|
return "{:.2f}s".format(seconds)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""Indirection for time functions.
|
||||||
Indirection for time functions.
|
|
||||||
|
|
||||||
We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
|
We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
|
||||||
pytest runtime information (issue #185).
|
pytest runtime information (issue #185).
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" support for providing temporary directories to test functions. """
|
"""Support for providing temporary directories to test functions."""
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -22,13 +22,14 @@ from _pytest.monkeypatch import MonkeyPatch
|
||||||
class TempPathFactory:
|
class TempPathFactory:
|
||||||
"""Factory for temporary directories under the common base temp directory.
|
"""Factory for temporary directories under the common base temp directory.
|
||||||
|
|
||||||
The base directory can be configured using the ``--basetemp`` option."""
|
The base directory can be configured using the ``--basetemp`` option.
|
||||||
|
"""
|
||||||
|
|
||||||
_given_basetemp = attr.ib(
|
_given_basetemp = attr.ib(
|
||||||
type=Path,
|
type=Path,
|
||||||
# using os.path.abspath() to get absolute path instead of resolve() as it
|
# Use os.path.abspath() to get absolute path instead of resolve() as it
|
||||||
# does not work the same in all platforms (see #4427)
|
# does not work the same in all platforms (see #4427).
|
||||||
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012)
|
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
|
||||||
# Ignore type because of https://github.com/python/mypy/issues/6172.
|
# Ignore type because of https://github.com/python/mypy/issues/6172.
|
||||||
converter=attr.converters.optional(
|
converter=attr.converters.optional(
|
||||||
lambda p: Path(os.path.abspath(str(p))) # type: ignore
|
lambda p: Path(os.path.abspath(str(p))) # type: ignore
|
||||||
|
@ -38,10 +39,8 @@ class TempPathFactory:
|
||||||
_basetemp = attr.ib(type=Optional[Path], default=None)
|
_basetemp = attr.ib(type=Optional[Path], default=None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(cls, config) -> "TempPathFactory":
|
def from_config(cls, config: Config) -> "TempPathFactory":
|
||||||
"""
|
"""Create a factory according to pytest configuration."""
|
||||||
:param config: a pytest configuration
|
|
||||||
"""
|
|
||||||
return cls(
|
return cls(
|
||||||
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
|
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
|
||||||
)
|
)
|
||||||
|
@ -55,7 +54,7 @@ class TempPathFactory:
|
||||||
return basename
|
return basename
|
||||||
|
|
||||||
def mktemp(self, basename: str, numbered: bool = True) -> Path:
|
def mktemp(self, basename: str, numbered: bool = True) -> Path:
|
||||||
"""Creates a new temporary directory managed by the factory.
|
"""Create a new temporary directory managed by the factory.
|
||||||
|
|
||||||
:param basename:
|
:param basename:
|
||||||
Directory base name, must be a relative path.
|
Directory base name, must be a relative path.
|
||||||
|
@ -66,7 +65,7 @@ class TempPathFactory:
|
||||||
means that this function will create directories named ``"foo-0"``,
|
means that this function will create directories named ``"foo-0"``,
|
||||||
``"foo-1"``, ``"foo-2"`` and so on.
|
``"foo-1"``, ``"foo-2"`` and so on.
|
||||||
|
|
||||||
:return:
|
:returns:
|
||||||
The path to the new directory.
|
The path to the new directory.
|
||||||
"""
|
"""
|
||||||
basename = self._ensure_relative_to_basetemp(basename)
|
basename = self._ensure_relative_to_basetemp(basename)
|
||||||
|
@ -79,7 +78,7 @@ class TempPathFactory:
|
||||||
return p
|
return p
|
||||||
|
|
||||||
def getbasetemp(self) -> Path:
|
def getbasetemp(self) -> Path:
|
||||||
""" return base temporary directory. """
|
"""Return base temporary directory."""
|
||||||
if self._basetemp is not None:
|
if self._basetemp is not None:
|
||||||
return self._basetemp
|
return self._basetemp
|
||||||
|
|
||||||
|
@ -106,28 +105,23 @@ class TempPathFactory:
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class TempdirFactory:
|
class TempdirFactory:
|
||||||
"""
|
"""Backward comptibility wrapper that implements :class:``py.path.local``
|
||||||
backward comptibility wrapper that implements
|
for :class:``TempPathFactory``."""
|
||||||
:class:``py.path.local`` for :class:``TempPathFactory``
|
|
||||||
"""
|
|
||||||
|
|
||||||
_tmppath_factory = attr.ib(type=TempPathFactory)
|
_tmppath_factory = attr.ib(type=TempPathFactory)
|
||||||
|
|
||||||
def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
|
def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
|
||||||
"""
|
"""Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object."""
|
||||||
Same as :meth:`TempPathFactory.mkdir`, but returns a ``py.path.local`` object.
|
|
||||||
"""
|
|
||||||
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
|
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
|
||||||
|
|
||||||
def getbasetemp(self) -> py.path.local:
|
def getbasetemp(self) -> py.path.local:
|
||||||
"""backward compat wrapper for ``_tmppath_factory.getbasetemp``"""
|
"""Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
|
||||||
return py.path.local(self._tmppath_factory.getbasetemp().resolve())
|
return py.path.local(self._tmppath_factory.getbasetemp().resolve())
|
||||||
|
|
||||||
|
|
||||||
def get_user() -> Optional[str]:
|
def get_user() -> Optional[str]:
|
||||||
"""Return the current user name, or None if getuser() does not work
|
"""Return the current user name, or None if getuser() does not work
|
||||||
in the current environment (see #1010).
|
in the current environment (see #1010)."""
|
||||||
"""
|
|
||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -153,16 +147,14 @@ def pytest_configure(config: Config) -> None:
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
|
def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
|
||||||
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
|
"""Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session."""
|
||||||
"""
|
|
||||||
# Set dynamically by pytest_configure() above.
|
# Set dynamically by pytest_configure() above.
|
||||||
return request.config._tmpdirhandler # type: ignore
|
return request.config._tmpdirhandler # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
|
||||||
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
|
"""Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session."""
|
||||||
"""
|
|
||||||
# Set dynamically by pytest_configure() above.
|
# Set dynamically by pytest_configure() above.
|
||||||
return request.config._tmp_path_factory # type: ignore
|
return request.config._tmp_path_factory # type: ignore
|
||||||
|
|
||||||
|
@ -177,11 +169,11 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tmpdir(tmp_path: Path) -> py.path.local:
|
def tmpdir(tmp_path: Path) -> py.path.local:
|
||||||
"""Return a temporary directory path object
|
"""Return a temporary directory path object which is unique to each test
|
||||||
which is unique to each test function invocation,
|
function invocation, created as a sub directory of the base temporary
|
||||||
created as a sub directory of the base temporary
|
directory.
|
||||||
directory. The returned object is a `py.path.local`_
|
|
||||||
path object.
|
The returned object is a `py.path.local`_ path object.
|
||||||
|
|
||||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||||
"""
|
"""
|
||||||
|
@ -190,15 +182,15 @@ def tmpdir(tmp_path: Path) -> py.path.local:
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
|
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
|
||||||
"""Return a temporary directory path object
|
"""Return a temporary directory path object which is unique to each test
|
||||||
which is unique to each test function invocation,
|
function invocation, created as a sub directory of the base temporary
|
||||||
created as a sub directory of the base temporary
|
directory.
|
||||||
directory. The returned object is a :class:`pathlib.Path`
|
|
||||||
object.
|
The returned object is a :class:`pathlib.Path` object.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
in python < 3.6 this is a pathlib2.Path
|
In python < 3.6 this is a pathlib2.Path.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return _mk_tmp(request, tmp_path_factory)
|
return _mk_tmp(request, tmp_path_factory)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
""" discovery and running of std-library "unittest" style tests. """
|
"""Discover and run std-library "unittest" style tests."""
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
|
@ -46,7 +46,7 @@ if TYPE_CHECKING:
|
||||||
def pytest_pycollect_makeitem(
|
def pytest_pycollect_makeitem(
|
||||||
collector: PyCollector, name: str, obj: object
|
collector: PyCollector, name: str, obj: object
|
||||||
) -> Optional["UnitTestCase"]:
|
) -> Optional["UnitTestCase"]:
|
||||||
# has unittest been imported and is obj a subclass of its TestCase?
|
# Has unittest been imported and is obj a subclass of its TestCase?
|
||||||
try:
|
try:
|
||||||
ut = sys.modules["unittest"]
|
ut = sys.modules["unittest"]
|
||||||
# Type ignored because `ut` is an opaque module.
|
# Type ignored because `ut` is an opaque module.
|
||||||
|
@ -54,14 +54,14 @@ def pytest_pycollect_makeitem(
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
# yes, so let's collect it
|
# Yes, so let's collect it.
|
||||||
item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase
|
item = UnitTestCase.from_parent(collector, name=name, obj=obj) # type: UnitTestCase
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
class UnitTestCase(Class):
|
class UnitTestCase(Class):
|
||||||
# marker for fixturemanger.getfixtureinfo()
|
# Marker for fixturemanger.getfixtureinfo()
|
||||||
# to declare that our children do not support funcargs
|
# to declare that our children do not support funcargs.
|
||||||
nofuncargs = True
|
nofuncargs = True
|
||||||
|
|
||||||
def collect(self) -> Iterable[Union[Item, Collector]]:
|
def collect(self) -> Iterable[Union[Item, Collector]]:
|
||||||
|
@ -97,7 +97,7 @@ class UnitTestCase(Class):
|
||||||
|
|
||||||
def _inject_setup_teardown_fixtures(self, cls: type) -> None:
|
def _inject_setup_teardown_fixtures(self, cls: type) -> None:
|
||||||
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
|
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
|
||||||
teardown functions (#517)"""
|
teardown functions (#517)."""
|
||||||
class_fixture = _make_xunit_fixture(
|
class_fixture = _make_xunit_fixture(
|
||||||
cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
|
cls, "setUpClass", "tearDownClass", scope="class", pass_self=False
|
||||||
)
|
)
|
||||||
|
@ -145,7 +145,7 @@ class TestCaseFunction(Function):
|
||||||
_testcase = None # type: Optional[unittest.TestCase]
|
_testcase = None # type: Optional[unittest.TestCase]
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
# a bound method to be called during teardown() if set (see 'runtest()')
|
# A bound method to be called during teardown() if set (see 'runtest()').
|
||||||
self._explicit_tearDown = None # type: Optional[Callable[[], None]]
|
self._explicit_tearDown = None # type: Optional[Callable[[], None]]
|
||||||
assert self.parent is not None
|
assert self.parent is not None
|
||||||
self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
|
self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
|
||||||
|
@ -164,12 +164,12 @@ class TestCaseFunction(Function):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
|
def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None:
|
||||||
# unwrap potential exception info (see twisted trial support below)
|
# Unwrap potential exception info (see twisted trial support below).
|
||||||
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
|
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
|
||||||
try:
|
try:
|
||||||
excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type]
|
excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type]
|
||||||
# invoke the attributes to trigger storing the traceback
|
# Invoke the attributes to trigger storing the traceback
|
||||||
# trial causes some issue there
|
# trial causes some issue there.
|
||||||
excinfo.value
|
excinfo.value
|
||||||
excinfo.traceback
|
excinfo.traceback
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -242,7 +242,7 @@ class TestCaseFunction(Function):
|
||||||
|
|
||||||
def _expecting_failure(self, test_method) -> bool:
|
def _expecting_failure(self, test_method) -> bool:
|
||||||
"""Return True if the given unittest method (or the entire class) is marked
|
"""Return True if the given unittest method (or the entire class) is marked
|
||||||
with @expectedFailure"""
|
with @expectedFailure."""
|
||||||
expecting_failure_method = getattr(
|
expecting_failure_method = getattr(
|
||||||
test_method, "__unittest_expecting_failure__", False
|
test_method, "__unittest_expecting_failure__", False
|
||||||
)
|
)
|
||||||
|
@ -256,23 +256,23 @@ class TestCaseFunction(Function):
|
||||||
|
|
||||||
maybe_wrap_pytest_function_for_tracing(self)
|
maybe_wrap_pytest_function_for_tracing(self)
|
||||||
|
|
||||||
# let the unittest framework handle async functions
|
# Let the unittest framework handle async functions.
|
||||||
if is_async_function(self.obj):
|
if is_async_function(self.obj):
|
||||||
# Type ignored because self acts as the TestResult, but is not actually one.
|
# Type ignored because self acts as the TestResult, but is not actually one.
|
||||||
self._testcase(result=self) # type: ignore[arg-type]
|
self._testcase(result=self) # type: ignore[arg-type]
|
||||||
else:
|
else:
|
||||||
# when --pdb is given, we want to postpone calling tearDown() otherwise
|
# When --pdb is given, we want to postpone calling tearDown() otherwise
|
||||||
# when entering the pdb prompt, tearDown() would have probably cleaned up
|
# when entering the pdb prompt, tearDown() would have probably cleaned up
|
||||||
# instance variables, which makes it difficult to debug
|
# instance variables, which makes it difficult to debug.
|
||||||
# arguably we could always postpone tearDown(), but this changes the moment where the
|
# Arguably we could always postpone tearDown(), but this changes the moment where the
|
||||||
# TestCase instance interacts with the results object, so better to only do it
|
# TestCase instance interacts with the results object, so better to only do it
|
||||||
# when absolutely needed
|
# when absolutely needed.
|
||||||
if self.config.getoption("usepdb") and not _is_skipped(self.obj):
|
if self.config.getoption("usepdb") and not _is_skipped(self.obj):
|
||||||
self._explicit_tearDown = self._testcase.tearDown
|
self._explicit_tearDown = self._testcase.tearDown
|
||||||
setattr(self._testcase, "tearDown", lambda *args: None)
|
setattr(self._testcase, "tearDown", lambda *args: None)
|
||||||
|
|
||||||
# we need to update the actual bound method with self.obj, because
|
# We need to update the actual bound method with self.obj, because
|
||||||
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper
|
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
|
||||||
setattr(self._testcase, self.name, self.obj)
|
setattr(self._testcase, self.name, self.obj)
|
||||||
try:
|
try:
|
||||||
self._testcase(result=self) # type: ignore[arg-type]
|
self._testcase(result=self) # type: ignore[arg-type]
|
||||||
|
@ -305,14 +305,14 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
|
||||||
and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
|
and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
|
||||||
):
|
):
|
||||||
excinfo = call.excinfo
|
excinfo = call.excinfo
|
||||||
# let's substitute the excinfo with a pytest.skip one
|
# Let's substitute the excinfo with a pytest.skip one.
|
||||||
call2 = CallInfo[None].from_call(
|
call2 = CallInfo[None].from_call(
|
||||||
lambda: pytest.skip(str(excinfo.value)), call.when
|
lambda: pytest.skip(str(excinfo.value)), call.when
|
||||||
)
|
)
|
||||||
call.excinfo = call2.excinfo
|
call.excinfo = call2.excinfo
|
||||||
|
|
||||||
|
|
||||||
# twisted trial support
|
# Twisted trial support.
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(hookwrapper=True)
|
||||||
|
@ -356,5 +356,5 @@ def check_testcase_implements_trial_reporter(done: List[int] = []) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _is_skipped(obj) -> bool:
|
def _is_skipped(obj) -> bool:
|
||||||
"""Return True if the given object has been marked with @unittest.skip"""
|
"""Return True if the given object has been marked with @unittest.skip."""
|
||||||
return bool(getattr(obj, "__unittest_skip__", False))
|
return bool(getattr(obj, "__unittest_skip__", False))
|
||||||
|
|
|
@ -99,7 +99,7 @@ class UnformattedWarning(Generic[_W]):
|
||||||
template = attr.ib(type=str)
|
template = attr.ib(type=str)
|
||||||
|
|
||||||
def format(self, **kwargs: Any) -> _W:
|
def format(self, **kwargs: Any) -> _W:
|
||||||
"""Returns an instance of the warning category, formatted with given kwargs"""
|
"""Return an instance of the warning category, formatted with given kwargs."""
|
||||||
return self.category(self.template.format(**kwargs))
|
return self.category(self.template.format(**kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -87,8 +87,7 @@ def catch_warnings_for_item(
|
||||||
when: "Literal['config', 'collect', 'runtest']",
|
when: "Literal['config', 'collect', 'runtest']",
|
||||||
item: Optional[Item],
|
item: Optional[Item],
|
||||||
) -> Generator[None, None, None]:
|
) -> Generator[None, None, None]:
|
||||||
"""
|
"""Context manager that catches warnings generated in the contained execution block.
|
||||||
Context manager that catches warnings generated in the contained execution block.
|
|
||||||
|
|
||||||
``item`` can be None if we are not in the context of an item execution.
|
``item`` can be None if we are not in the context of an item execution.
|
||||||
|
|
||||||
|
@ -101,14 +100,14 @@ def catch_warnings_for_item(
|
||||||
assert log is not None
|
assert log is not None
|
||||||
|
|
||||||
if not sys.warnoptions:
|
if not sys.warnoptions:
|
||||||
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
|
# If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
|
||||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||||
|
|
||||||
warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning)
|
warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning)
|
||||||
|
|
||||||
# filters should have this precedence: mark, cmdline options, ini
|
# Filters should have this precedence: mark, cmdline options, ini.
|
||||||
# filters should be applied in the inverse order of precedence
|
# Filters should be applied in the inverse order of precedence.
|
||||||
for arg in inifilters:
|
for arg in inifilters:
|
||||||
warnings.filterwarnings(*_parse_filter(arg, escape=False))
|
warnings.filterwarnings(*_parse_filter(arg, escape=False))
|
||||||
|
|
||||||
|
@ -193,14 +192,16 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]:
|
||||||
|
|
||||||
|
|
||||||
def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None:
|
def _issue_warning_captured(warning: Warning, hook, stacklevel: int) -> None:
|
||||||
"""
|
"""A function that should be used instead of calling ``warnings.warn``
|
||||||
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
|
directly when we are in the "configure" stage.
|
||||||
at this point the actual options might not have been set, so we manually trigger the pytest_warning_recorded
|
|
||||||
hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891.
|
|
||||||
|
|
||||||
:param warning: the warning instance.
|
At this point the actual options might not have been set, so we manually
|
||||||
:param hook: the hook caller
|
trigger the pytest_warning_recorded hook so we can display these warnings
|
||||||
:param stacklevel: stacklevel forwarded to warnings.warn
|
in the terminal. This is a hack until we can sort out #2891.
|
||||||
|
|
||||||
|
:param warning: The warning instance.
|
||||||
|
:param hook: The hook caller.
|
||||||
|
:param stacklevel: stacklevel forwarded to warnings.warn.
|
||||||
"""
|
"""
|
||||||
with warnings.catch_warnings(record=True) as records:
|
with warnings.catch_warnings(record=True) as records:
|
||||||
warnings.simplefilter("always", type(warning))
|
warnings.simplefilter("always", type(warning))
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# PYTHON_ARGCOMPLETE_OK
|
# PYTHON_ARGCOMPLETE_OK
|
||||||
"""
|
"""pytest: unit and functional testing with Python."""
|
||||||
pytest: unit and functional testing with Python.
|
|
||||||
"""
|
|
||||||
from . import collect
|
from . import collect
|
||||||
from _pytest import __version__
|
from _pytest import __version__
|
||||||
from _pytest.assertion import register_assert_rewrite
|
from _pytest.assertion import register_assert_rewrite
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
"""
|
"""The pytest entry point."""
|
||||||
pytest entry point
|
|
||||||
"""
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -2,12 +2,14 @@ import os
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import cast
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest import pathlib
|
from _pytest import pathlib
|
||||||
|
from _pytest.config import Config
|
||||||
from _pytest.pathlib import cleanup_numbered_dir
|
from _pytest.pathlib import cleanup_numbered_dir
|
||||||
from _pytest.pathlib import create_cleanup_lock
|
from _pytest.pathlib import create_cleanup_lock
|
||||||
from _pytest.pathlib import make_numbered_dir
|
from _pytest.pathlib import make_numbered_dir
|
||||||
|
@ -45,7 +47,7 @@ class FakeConfig:
|
||||||
|
|
||||||
class TestTempdirHandler:
|
class TestTempdirHandler:
|
||||||
def test_mktemp(self, tmp_path):
|
def test_mktemp(self, tmp_path):
|
||||||
config = FakeConfig(tmp_path)
|
config = cast(Config, FakeConfig(tmp_path))
|
||||||
t = TempdirFactory(TempPathFactory.from_config(config))
|
t = TempdirFactory(TempPathFactory.from_config(config))
|
||||||
tmp = t.mktemp("world")
|
tmp = t.mktemp("world")
|
||||||
assert tmp.relto(t.getbasetemp()) == "world0"
|
assert tmp.relto(t.getbasetemp()) == "world0"
|
||||||
|
@ -58,7 +60,7 @@ class TestTempdirHandler:
|
||||||
def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch):
|
def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch):
|
||||||
"""#4425"""
|
"""#4425"""
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
config = FakeConfig("hello")
|
config = cast(Config, FakeConfig("hello"))
|
||||||
t = TempPathFactory.from_config(config)
|
t = TempPathFactory.from_config(config)
|
||||||
assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve()
|
assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue