diff options
author | deshevoy <deshevoy@yandex-team.ru> | 2022-02-10 16:46:57 +0300 |
---|---|---|
committer | Daniil Cherednik <dcherednik@yandex-team.ru> | 2022-02-10 16:46:57 +0300 |
commit | 28148f76dbfcc644d96427d41c92f36cbf2fdc6e (patch) | |
tree | b83306b6e37edeea782e9eed673d89286c4fef35 /contrib/python/pytest/py3 | |
parent | e988f30484abe5fdeedcc7a5d3c226c01a21800c (diff) | |
download | ydb-28148f76dbfcc644d96427d41c92f36cbf2fdc6e.tar.gz |
Restoring authorship annotation for <deshevoy@yandex-team.ru>. Commit 2 of 2.
Diffstat (limited to 'contrib/python/pytest/py3')
52 files changed, 10722 insertions, 10722 deletions
diff --git a/contrib/python/pytest/py3/LICENSE b/contrib/python/pytest/py3/LICENSE index 958fc1d1c6..d14fb7ff4b 100644 --- a/contrib/python/pytest/py3/LICENSE +++ b/contrib/python/pytest/py3/LICENSE @@ -1,21 +1,21 @@ -The MIT License (MIT) - +The MIT License (MIT) + Copyright (c) 2004-2020 Holger Krekel and others - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contrib/python/pytest/py3/_pytest/__init__.py b/contrib/python/pytest/py3/_pytest/__init__.py index f7e52f229e..46c7827ed5 100644 --- a/contrib/python/pytest/py3/_pytest/__init__.py +++ b/contrib/python/pytest/py3/_pytest/__init__.py @@ -1,8 +1,8 @@ -__all__ = ["__version__"] - -try: - from ._version import version as __version__ -except ImportError: - # broken installation, we don't even try - # unknown only works because we do poor mans version compare - __version__ = "unknown" +__all__ = ["__version__"] + +try: + from ._version import version as __version__ +except ImportError: + # broken installation, we don't even try + # unknown only works because we do poor mans version compare + __version__ = "unknown" diff --git a/contrib/python/pytest/py3/_pytest/_argcomplete.py b/contrib/python/pytest/py3/_pytest/_argcomplete.py index 508f65d5d0..41d9d9407c 100644 --- a/contrib/python/pytest/py3/_pytest/_argcomplete.py +++ b/contrib/python/pytest/py3/_pytest/_argcomplete.py @@ -1,117 +1,117 @@ """Allow bash-completion for argparse with argcomplete if installed. 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). - -Function try_argcomplete(parser) should be called directly before -the call to ArgumentParser.parse_args(). - -The filescompleter is what you normally would use on the positional -arguments specification, in order to get "dirname/" after "dirn<TAB>" -instead of the default "dirname ": - + +Function try_argcomplete(parser) should be called directly before +the call to ArgumentParser.parse_args(). + +The filescompleter is what you normally would use on the positional +arguments specification, in order to get "dirname/" after "dirn<TAB>" +instead of the default "dirname ": + optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter - -Other, application specific, completers should go in the file -doing the add_argument calls as they need to be specified as .completer -attributes as well. (If argcomplete is not installed, the function the -attribute points to will not be used). - -SPEEDUP -======= - -The generic argcomplete script for bash-completion + +Other, application specific, completers should go in the file +doing the add_argument calls as they need to be specified as .completer +attributes as well. (If argcomplete is not installed, the function the +attribute points to will not be used). + +SPEEDUP +======= + +The generic argcomplete script for bash-completion (/etc/bash_completion.d/python-argcomplete.sh) -uses a python program to determine startup script generated by pip. -You can speed up completion somewhat by changing this script to include - # PYTHON_ARGCOMPLETE_OK +uses a python program to determine startup script generated by pip. +You can speed up completion somewhat by changing this script to include + # PYTHON_ARGCOMPLETE_OK so 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. - -INSTALL/DEBUGGING -================= -To include this support in another application that has setup.py generated -scripts: +INSTALL/DEBUGGING +================= + +To include this support in another application that has setup.py generated +scripts: - Add the line: - # PYTHON_ARGCOMPLETE_OK + # PYTHON_ARGCOMPLETE_OK near the top of the main python entry point. - 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 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 - completers): - export _ARC_DEBUG=1 + completers): + export _ARC_DEBUG=1 - Run: - python-argcomplete-check-easy-install-script $(which appname) - echo $? + python-argcomplete-check-easy-install-script $(which appname) + echo $? will echo 0 if the magic line has been found, 1 if not. - Sometimes it helps to find early on errors using: - _ARGCOMPLETE=1 _ARC_DEBUG=1 appname - which should throw a KeyError: 'COMPLINE' (which is properly set by the - global argcomplete script). -""" + _ARGCOMPLETE=1 _ARC_DEBUG=1 appname + which should throw a KeyError: 'COMPLINE' (which is properly set by the + global argcomplete script). +""" import argparse -import os -import sys -from glob import glob +import os +import sys +from glob import glob from typing import Any from typing import List from typing import Optional - - + + class FastFilesCompleter: """Fast file completer class.""" - + def __init__(self, directories: bool = True) -> None: - self.directories = directories - + self.directories = directories + def __call__(self, prefix: str, **kwargs: Any) -> List[str]: # Only called on non option completions. - if os.path.sep in prefix[1:]: - prefix_dir = len(os.path.dirname(prefix) + os.path.sep) - else: - prefix_dir = 0 - completion = [] - globbed = [] - if "*" not in prefix and "?" not in prefix: + if os.path.sep in prefix[1:]: + prefix_dir = len(os.path.dirname(prefix) + os.path.sep) + else: + prefix_dir = 0 + completion = [] + globbed = [] + if "*" not in prefix and "?" not in prefix: # We are on unix, otherwise no bash. - if not prefix or prefix[-1] == os.path.sep: - globbed.extend(glob(prefix + ".*")) - prefix += "*" - globbed.extend(glob(prefix)) - for x in sorted(globbed): - if os.path.isdir(x): - x += "/" + if not prefix or prefix[-1] == os.path.sep: + globbed.extend(glob(prefix + ".*")) + prefix += "*" + globbed.extend(glob(prefix)) + for x in sorted(globbed): + if os.path.isdir(x): + x += "/" # Append stripping the prefix (like bash, not like compgen). - completion.append(x[prefix_dir:]) - return completion - - -if os.environ.get("_ARGCOMPLETE"): - try: - import argcomplete.completers - except ImportError: - sys.exit(-1) + completion.append(x[prefix_dir:]) + return completion + + +if os.environ.get("_ARGCOMPLETE"): + try: + import argcomplete.completers + except ImportError: + sys.exit(-1) filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter() - + def try_argcomplete(parser: argparse.ArgumentParser) -> None: - argcomplete.autocomplete(parser, always_complete_options=False) - - -else: - + argcomplete.autocomplete(parser, always_complete_options=False) + + +else: + def try_argcomplete(parser: argparse.ArgumentParser) -> None: - pass - - filescompleter = None + pass + + filescompleter = None diff --git a/contrib/python/pytest/py3/_pytest/_code/code.py b/contrib/python/pytest/py3/_pytest/_code/code.py index 576a491d70..423069330a 100644 --- a/contrib/python/pytest/py3/_pytest/_code/code.py +++ b/contrib/python/pytest/py3/_pytest/_code/code.py @@ -1,9 +1,9 @@ -import inspect -import re -import sys -import traceback -from inspect import CO_VARARGS -from inspect import CO_VARKEYWORDS +import inspect +import re +import sys +import traceback +from inspect import CO_VARARGS +from inspect import CO_VARKEYWORDS from io import StringIO from pathlib import Path from traceback import format_exception_only @@ -27,13 +27,13 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -from weakref import ref - -import attr -import pluggy -import py - -import _pytest +from weakref import ref + +import attr +import pluggy +import py + +import _pytest from _pytest._code.source import findsource from _pytest._code.source import getrawcode from _pytest._code.source import getstatementrange_ast @@ -43,19 +43,19 @@ from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr from _pytest.compat import final from _pytest.compat import get_real_func - + if TYPE_CHECKING: from typing_extensions import Literal from weakref import ReferenceType - + _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] - + class Code: """Wrapper around Python code objects.""" - + __slots__ = ("raw",) - + def __init__(self, obj: CodeType) -> None: self.raw = obj @@ -63,17 +63,17 @@ class Code: def from_function(cls, obj: object) -> "Code": return cls(getrawcode(obj)) - def __eq__(self, other): - return self.raw == other.raw - + def __eq__(self, other): + return self.raw == other.raw + # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore - + @property def firstlineno(self) -> int: return self.raw.co_firstlineno - 1 - - @property + + @property def name(self) -> str: return self.raw.co_name @@ -83,53 +83,53 @@ class Code: case of ``OSError`` / non-existing file.""" if not self.raw.co_filename: return "" - try: - p = py.path.local(self.raw.co_filename) - # maybe don't try this checking - if not p.check(): - raise OSError("py.path check failed.") + try: + p = py.path.local(self.raw.co_filename) + # maybe don't try this checking + if not p.check(): + raise OSError("py.path check failed.") return p - except OSError: - # XXX maybe try harder like the weird logic - # in the standard lib [linecache.updatecache] does? + except OSError: + # XXX maybe try harder like the weird logic + # in the standard lib [linecache.updatecache] does? return self.raw.co_filename - - @property + + @property def fullsource(self) -> Optional["Source"]: """Return a _pytest._code.Source object for the full source file of the code.""" full, _ = findsource(self.raw) - return full - + return full + def source(self) -> "Source": """Return a _pytest._code.Source object for the code object's source only.""" - # return source only for that part of code + # return source only for that part of code return Source(self.raw) - + def getargs(self, var: bool = False) -> Tuple[str, ...]: """Return a tuple with the argument names for the code object. - + If 'var' is set True also return the names of the variable and keyword arguments when present. - """ + """ # Handy shortcut for getting args. - raw = self.raw - argcount = raw.co_argcount - if var: - argcount += raw.co_flags & CO_VARARGS - argcount += raw.co_flags & CO_VARKEYWORDS - return raw.co_varnames[:argcount] - - + raw = self.raw + argcount = raw.co_argcount + if var: + argcount += raw.co_flags & CO_VARARGS + argcount += raw.co_flags & CO_VARKEYWORDS + return raw.co_varnames[:argcount] + + class Frame: - """Wrapper around a Python frame holding f_locals and f_globals - in which expressions can be evaluated.""" - + """Wrapper around a Python frame holding f_locals and f_globals + in which expressions can be evaluated.""" + __slots__ = ("raw",) def __init__(self, frame: FrameType) -> None: - self.raw = frame - - @property + self.raw = frame + + @property def lineno(self) -> int: return self.raw.f_lineno - 1 @@ -148,128 +148,128 @@ class Frame: @property def statement(self) -> "Source": """Statement this frame is at.""" - if self.code.fullsource is None: + if self.code.fullsource is None: return Source("") - return self.code.fullsource.getstatement(self.lineno) - - def eval(self, code, **vars): + return self.code.fullsource.getstatement(self.lineno) + + def eval(self, code, **vars): """Evaluate 'code' in the frame. - + 'vars' are optional additional local variables. - + Returns the result of the evaluation. - """ - f_locals = self.f_locals.copy() - f_locals.update(vars) - return eval(code, self.f_globals, f_locals) - + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) + return eval(code, self.f_globals, f_locals) + def repr(self, object: object) -> str: """Return a 'safe' (non-recursive, one-line) string repr for 'object'.""" return saferepr(object) - + def getargs(self, var: bool = False): """Return a list of tuples (name, value) for all arguments. - + If 'var' is set True, also include the variable and keyword arguments when present. - """ - retval = [] - for arg in self.code.getargs(var): - try: - retval.append((arg, self.f_locals[arg])) - except KeyError: - pass # this can occur when using Psyco - return retval - - + """ + retval = [] + for arg in self.code.getargs(var): + try: + retval.append((arg, self.f_locals[arg])) + except KeyError: + pass # this can occur when using Psyco + return retval + + class TracebackEntry: """A single entry in a Traceback.""" - + __slots__ = ("_rawentry", "_excinfo", "_repr_style") - + def __init__( self, rawentry: TracebackType, excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, ) -> None: self._rawentry = rawentry - self._excinfo = excinfo + self._excinfo = excinfo self._repr_style: Optional['Literal["short", "long"]'] = None - + @property def lineno(self) -> int: return self._rawentry.tb_lineno - 1 def set_repr_style(self, mode: "Literal['short', 'long']") -> None: - assert mode in ("short", "long") - self._repr_style = mode - - @property + assert mode in ("short", "long") + self._repr_style = mode + + @property def frame(self) -> Frame: return Frame(self._rawentry.tb_frame) - - @property + + @property def relline(self) -> int: - return self.lineno - self.frame.code.firstlineno - + return self.lineno - self.frame.code.firstlineno + def __repr__(self) -> str: - return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1) - - @property + return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1) + + @property def statement(self) -> "Source": """_pytest._code.Source object for the current statement.""" - source = self.frame.code.fullsource + source = self.frame.code.fullsource assert source is not None - return source.getstatement(self.lineno) - - @property + return source.getstatement(self.lineno) + + @property def path(self) -> Union[py.path.local, str]: """Path to the source code.""" - return self.frame.code.path - + return self.frame.code.path + @property def locals(self) -> Dict[str, Any]: """Locals of underlying frame.""" - return self.frame.f_locals - + return self.frame.f_locals + def getfirstlinesource(self) -> int: return self.frame.code.firstlineno - + def getsource(self, astcache=None) -> Optional["Source"]: """Return failing source code.""" - # we use the passed in astcache to not reparse asttrees - # within exception info printing - source = self.frame.code.fullsource - if source is None: - return None - key = astnode = None - if astcache is not None: - key = self.frame.code.path - if key is not None: - astnode = astcache.get(key, None) - start = self.getfirstlinesource() - try: - astnode, _, end = getstatementrange_ast( - self.lineno, source, astnode=astnode - ) - except SyntaxError: - end = self.lineno + 1 - else: - if key is not None: - astcache[key] = astnode - return source[start:end] - - source = property(getsource) - + # we use the passed in astcache to not reparse asttrees + # within exception info printing + source = self.frame.code.fullsource + if source is None: + return None + key = astnode = None + if astcache is not None: + key = self.frame.code.path + if key is not None: + astnode = astcache.get(key, None) + start = self.getfirstlinesource() + try: + astnode, _, end = getstatementrange_ast( + self.lineno, source, astnode=astnode + ) + except SyntaxError: + end = self.lineno + 1 + else: + if key is not None: + astcache[key] = astnode + return source[start:end] + + source = property(getsource) + def ishidden(self) -> bool: """Return True if the current frame has a var __tracebackhide__ resolving to True. - + If __tracebackhide__ is a callable, it gets called with the ExceptionInfo instance and can decide whether to hide the traceback. - + Mostly for internal use. - """ + """ tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = ( False ) @@ -285,17 +285,17 @@ class TracebackEntry: else: break if tbh and callable(tbh): - return tbh(None if self._excinfo is None else self._excinfo()) + return tbh(None if self._excinfo is None else self._excinfo()) return tbh - + def __str__(self) -> str: - name = self.frame.code.name - try: - line = str(self.statement).lstrip() - except KeyboardInterrupt: - raise + name = self.frame.code.name + try: + line = str(self.statement).lstrip() + except KeyboardInterrupt: + raise except BaseException: - line = "???" + line = "???" # This output does not quite match Python's repr for traceback entries, # but changing it to do so would break certain plugins. See # https://github.com/pytest-dev/pytest/pull/7535/ for details. @@ -305,35 +305,35 @@ class TracebackEntry: name, line, ) - + @property def name(self) -> str: """co_name of underlying code.""" - return self.frame.code.raw.co_name - - + return self.frame.code.raw.co_name + + class Traceback(List[TracebackEntry]): """Traceback objects encapsulate and offer higher level access to Traceback entries.""" - + def __init__( self, tb: Union[TracebackType, Iterable[TracebackEntry]], excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, ) -> None: """Initialize from given python traceback object and ExceptionInfo.""" - self._excinfo = excinfo + self._excinfo = excinfo if isinstance(tb, TracebackType): - + def f(cur: TracebackType) -> Iterable[TracebackEntry]: cur_: Optional[TracebackType] = cur while cur_ is not None: yield TracebackEntry(cur_, excinfo=excinfo) cur_ = cur_.tb_next - + super().__init__(f(tb)) - else: + else: super().__init__(tb) - + def cut( self, path=None, @@ -342,34 +342,34 @@ class Traceback(List[TracebackEntry]): excludepath: Optional[py.path.local] = None, ) -> "Traceback": """Return a Traceback instance wrapping part of this Traceback. - + By providing any combination of path, lineno and firstlineno, the first frame to start the to-be-returned traceback is determined. - + This allows cutting the first part of a Traceback instance e.g. for formatting reasons (removing some uninteresting bits that deal with handling of the exception/traceback). - """ - for x in self: - code = x.frame.code - codepath = code.path - if ( - (path is None or codepath == path) - and ( - excludepath is None + """ + for x in self: + code = x.frame.code + codepath = code.path + if ( + (path is None or codepath == path) + and ( + excludepath is None or not isinstance(codepath, py.path.local) - or not codepath.relto(excludepath) - ) - and (lineno is None or x.lineno == lineno) - and (firstlineno is None or x.frame.code.firstlineno == firstlineno) - ): - return Traceback(x._rawentry, self._excinfo) - return self - + or not codepath.relto(excludepath) + ) + and (lineno is None or x.lineno == lineno) + and (firstlineno is None or x.frame.code.firstlineno == firstlineno) + ): + return Traceback(x._rawentry, self._excinfo) + return self + @overload def __getitem__(self, key: int) -> TracebackEntry: ... - + @overload def __getitem__(self, key: slice) -> "Traceback": ... @@ -384,55 +384,55 @@ class Traceback(List[TracebackEntry]): self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() ) -> "Traceback": """Return a Traceback instance with certain items removed - + fn is a function that gets a single argument, a TracebackEntry instance, and should return True when the item should be added to the Traceback, False when not. - + By default this removes all the TracebackEntries which are hidden (see ishidden() above). - """ - return Traceback(filter(fn, self), self._excinfo) - + """ + return Traceback(filter(fn, self), self._excinfo) + def getcrashentry(self) -> TracebackEntry: """Return last non-hidden traceback entry that lead to the exception of a traceback.""" - for i in range(-1, -len(self) - 1, -1): - entry = self[i] - if not entry.ishidden(): - return entry - return self[-1] - + for i in range(-1, -len(self) - 1, -1): + entry = self[i] + if not entry.ishidden(): + return entry + return self[-1] + def recursionindex(self) -> Optional[int]: """Return the index of the frame/TracebackEntry where recursion originates if appropriate, None if no recursion occurred.""" cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {} - for i, entry in enumerate(self): - # id for the code.raw is needed to work around - # the strange metaprogramming in the decorator lib from pypi - # which generates code objects that have hash/value equality - # XXX needs a test - key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno - # print "checking for recursion at", key - values = cache.setdefault(key, []) - if values: - f = entry.frame - loc = f.f_locals - for otherloc in values: + for i, entry in enumerate(self): + # id for the code.raw is needed to work around + # the strange metaprogramming in the decorator lib from pypi + # which generates code objects that have hash/value equality + # XXX needs a test + key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno + # print "checking for recursion at", key + values = cache.setdefault(key, []) + if values: + f = entry.frame + loc = f.f_locals + for otherloc in values: if f.eval( co_equal, __recursioncache_locals_1=loc, __recursioncache_locals_2=otherloc, - ): - return i - values.append(entry.frame.f_locals) - return None - - -co_equal = compile( - "__recursioncache_locals_1 == __recursioncache_locals_2", "?", "eval" -) - - + ): + return i + values.append(entry.frame.f_locals) + return None + + +co_equal = compile( + "__recursioncache_locals_1 == __recursioncache_locals_2", "?", "eval" +) + + _E = TypeVar("_E", bound=BaseException, covariant=True) @@ -440,13 +440,13 @@ _E = TypeVar("_E", bound=BaseException, covariant=True) @attr.s(repr=False) class ExceptionInfo(Generic[_E]): """Wraps sys.exc_info() objects and offers help for navigating the traceback.""" - + _assert_start_repr = "AssertionError('assert " - + _excinfo = attr.ib(type=Optional[Tuple[Type["_E"], "_E", TracebackType]]) _striptext = attr.ib(type=str, default="") _traceback = attr.ib(type=Optional[Traceback], default=None) - + @classmethod def from_exc_info( cls, @@ -454,7 +454,7 @@ class ExceptionInfo(Generic[_E]): exprinfo: Optional[str] = None, ) -> "ExceptionInfo[_E]": """Return an ExceptionInfo for an existing exc_info tuple. - + .. warning:: Experimental API @@ -555,23 +555,23 @@ class ExceptionInfo(Generic[_E]): return "<{} {} tblen={}>".format( self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback) ) - + def exconly(self, tryshort: bool = False) -> str: """Return the exception as a string. - + When 'tryshort' resolves to True, and the exception is a _pytest._code._AssertionError, only the actual exception part of the exception representation is returned (so 'AssertionError: ' is removed from the beginning). - """ - lines = format_exception_only(self.type, self.value) - text = "".join(lines) - text = text.rstrip() - if tryshort: - if text.startswith(self._striptext): - text = text[len(self._striptext) :] - return text - + """ + lines = format_exception_only(self.type, self.value) + text = "".join(lines) + text = text.rstrip() + if tryshort: + if text.startswith(self._striptext): + text = text[len(self._striptext) :] + return text + def errisinstance( self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]] ) -> bool: @@ -579,16 +579,16 @@ class ExceptionInfo(Generic[_E]): Consider using ``isinstance(excinfo.value, exc)`` instead. """ - return isinstance(self.value, exc) - + return isinstance(self.value, exc) + def _getreprcrash(self) -> "ReprFileLocation": - exconly = self.exconly(tryshort=True) - entry = self.traceback.getcrashentry() - path, lineno = entry.frame.code.raw.co_filename, entry.lineno - return ReprFileLocation(path, lineno + 1, exconly) - - def getrepr( - self, + exconly = self.exconly(tryshort=True) + entry = self.traceback.getcrashentry() + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + return ReprFileLocation(path, lineno + 1, exconly) + + def getrepr( + self, showlocals: bool = False, style: "_TracebackStyle" = "long", abspath: bool = False, @@ -598,78 +598,78 @@ class ExceptionInfo(Generic[_E]): chain: bool = True, ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]: """Return str()able representation of this exception info. - - :param bool showlocals: - Show locals per traceback entry. - Ignored if ``style=="native"``. - + + :param bool showlocals: + Show locals per traceback entry. + Ignored if ``style=="native"``. + :param str style: long|short|no|native|value traceback style. - - :param bool abspath: - If paths should be changed to absolute or left unchanged. - - :param bool tbfilter: - Hide entries that contain a local variable ``__tracebackhide__==True``. - Ignored if ``style=="native"``. - - :param bool funcargs: - Show fixtures ("funcargs" for legacy purposes) per traceback entry. - - :param bool truncate_locals: - With ``showlocals==True``, make sure locals can be safely represented as strings. - + + :param bool abspath: + If paths should be changed to absolute or left unchanged. + + :param bool tbfilter: + Hide entries that contain a local variable ``__tracebackhide__==True``. + Ignored if ``style=="native"``. + + :param bool funcargs: + Show fixtures ("funcargs" for legacy purposes) per traceback entry. + + :param bool truncate_locals: + With ``showlocals==True``, make sure locals can be safely represented as strings. + :param bool chain: If chained exceptions in Python 3 should be shown. - - .. versionchanged:: 3.9 - - Added the ``chain`` parameter. - """ - if style == "native": - return ReprExceptionInfo( - ReprTracebackNative( - traceback.format_exception( - self.type, self.value, self.traceback[0]._rawentry - ) - ), - self._getreprcrash(), - ) - - fmt = FormattedExcinfo( - showlocals=showlocals, - style=style, - abspath=abspath, - tbfilter=tbfilter, - funcargs=funcargs, - truncate_locals=truncate_locals, - chain=chain, - ) - return fmt.repr_excinfo(self) - + + .. versionchanged:: 3.9 + + Added the ``chain`` parameter. + """ + if style == "native": + return ReprExceptionInfo( + ReprTracebackNative( + traceback.format_exception( + self.type, self.value, self.traceback[0]._rawentry + ) + ), + self._getreprcrash(), + ) + + fmt = FormattedExcinfo( + showlocals=showlocals, + style=style, + abspath=abspath, + tbfilter=tbfilter, + funcargs=funcargs, + truncate_locals=truncate_locals, + chain=chain, + ) + return fmt.repr_excinfo(self) + def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]": """Check whether the regular expression `regexp` matches the string representation of the exception using :func:`python:re.search`. If it matches `True` is returned, otherwise an `AssertionError` is raised. - """ - __tracebackhide__ = True + """ + __tracebackhide__ = True msg = "Regex pattern {!r} does not match {!r}." if regexp == str(self.value): msg += " Did you mean to `re.escape()` the regex?" assert re.search(regexp, str(self.value)), msg.format(regexp, str(self.value)) # Return True to allow for "assert excinfo.match()". - return True - - -@attr.s + return True + + +@attr.s class FormattedExcinfo: """Presenting information about failing Functions and Generators.""" - - # for traceback entries - flow_marker = ">" - fail_marker = "E" - + + # for traceback entries + flow_marker = ">" + fail_marker = "E" + showlocals = attr.ib(type=bool, default=False) style = attr.ib(type="_TracebackStyle", default="long") abspath = attr.ib(type=bool, default=True) @@ -677,37 +677,37 @@ class FormattedExcinfo: funcargs = attr.ib(type=bool, default=False) truncate_locals = attr.ib(type=bool, default=True) chain = attr.ib(type=bool, default=True) - 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: # Figure out indent for the given source. - try: - s = str(source.getstatement(len(source) - 1)) - except KeyboardInterrupt: - raise + try: + s = str(source.getstatement(len(source) - 1)) + except KeyboardInterrupt: + raise except BaseException: - try: - s = str(source[-1]) - except KeyboardInterrupt: - raise + try: + s = str(source[-1]) + except KeyboardInterrupt: + raise except BaseException: - return 0 - return 4 + (len(s) - len(s.lstrip())) - + return 0 + return 4 + (len(s) - len(s.lstrip())) + def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]: - source = entry.getsource(self.astcache) - if source is not None: - source = source.deindent() - return source - + source = entry.getsource(self.astcache) + if source is not None: + source = source.deindent() + return source + def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: - if self.funcargs: - args = [] - for argname, argvalue in entry.frame.getargs(var=True): + if self.funcargs: + args = [] + for argname, argvalue in entry.frame.getargs(var=True): args.append((argname, saferepr(argvalue))) - return ReprFuncArgs(args) + return ReprFuncArgs(args) return None - + def get_source( self, source: Optional["Source"], @@ -716,69 +716,69 @@ class FormattedExcinfo: short: bool = False, ) -> List[str]: """Return formatted and marked up source lines.""" - lines = [] - if source is None or line_index >= len(source.lines): + lines = [] + if source is None or line_index >= len(source.lines): source = Source("???") - line_index = 0 - if line_index < 0: - line_index += len(source) - space_prefix = " " - if short: - lines.append(space_prefix + source.lines[line_index].strip()) - else: - for line in source.lines[:line_index]: - lines.append(space_prefix + line) - lines.append(self.flow_marker + " " + source.lines[line_index]) - for line in source.lines[line_index + 1 :]: - lines.append(space_prefix + line) - if excinfo is not None: - indent = 4 if short else self._getindent(source) - lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) - return lines - + line_index = 0 + if line_index < 0: + line_index += len(source) + space_prefix = " " + if short: + lines.append(space_prefix + source.lines[line_index].strip()) + else: + for line in source.lines[:line_index]: + lines.append(space_prefix + line) + lines.append(self.flow_marker + " " + source.lines[line_index]) + for line in source.lines[line_index + 1 :]: + lines.append(space_prefix + line) + if excinfo is not None: + indent = 4 if short else self._getindent(source) + lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) + return lines + def get_exconly( self, excinfo: ExceptionInfo[BaseException], indent: int = 4, markall: bool = False, ) -> List[str]: - lines = [] + lines = [] indentstr = " " * indent # 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:] - for line in exlines: - lines.append(failindent + line) - if not markall: + for line in exlines: + lines.append(failindent + line) + if not markall: failindent = indentstr - return lines - + return lines + def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: - if self.showlocals: - lines = [] - keys = [loc for loc in locals if loc[0] != "@"] - keys.sort() - for name in keys: - value = locals[name] - if name == "__builtins__": - lines.append("__builtins__ = <builtins>") - else: - # This formatting could all be handled by the - # _repr() function, which is only reprlib.Repr in - # disguise, so is very configurable. - if self.truncate_locals: + if self.showlocals: + lines = [] + keys = [loc for loc in locals if loc[0] != "@"] + keys.sort() + for name in keys: + value = locals[name] + if name == "__builtins__": + lines.append("__builtins__ = <builtins>") + else: + # This formatting could all be handled by the + # _repr() function, which is only reprlib.Repr in + # disguise, so is very configurable. + if self.truncate_locals: str_repr = saferepr(value) - else: + else: str_repr = safeformat(value) # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)): lines.append(f"{name:<10} = {str_repr}") - # else: - # self._line("%-10s =\\" % (name,)) - # # XXX - # pprint.pprint(value, stream=self.excinfowriter) - return ReprLocals(lines) + # else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) return None - + def repr_traceback_entry( self, entry: TracebackEntry, @@ -786,22 +786,22 @@ class FormattedExcinfo: ) -> "ReprEntry": lines: List[str] = [] style = entry._repr_style if entry._repr_style is not None else self.style - if style in ("short", "long"): + if style in ("short", "long"): source = self._getentrysource(entry) if source is None: source = Source("???") line_index = 0 else: line_index = entry.lineno - entry.getfirstlinesource() - short = style == "short" - reprargs = self.repr_args(entry) if not short else None - s = self.get_source(source, line_index, excinfo, short=short) - lines.extend(s) - if short: - message = "in %s" % (entry.name) - else: - message = excinfo and excinfo.typename or "" - path = self._makepath(entry.path) + short = style == "short" + reprargs = self.repr_args(entry) if not short else None + s = self.get_source(source, line_index, excinfo, short=short) + lines.extend(s) + if short: + message = "in %s" % (entry.name) + else: + message = excinfo and excinfo.typename or "" + path = self._makepath(entry.path) reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) localsrepr = self.repr_locals(entry.locals) return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style) @@ -813,82 +813,82 @@ class FormattedExcinfo: if excinfo: lines.extend(self.get_exconly(excinfo, indent=4)) return ReprEntry(lines, None, None, None, style) - - def _makepath(self, path): - if not self.abspath: - try: - np = py.path.local().bestrelpath(path) - except OSError: - return path - if len(np) < len(str(path)): - path = np - return path - + + def _makepath(self, path): + if not self.abspath: + try: + np = py.path.local().bestrelpath(path) + except OSError: + return path + if len(np) < len(str(path)): + path = np + return path + def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": - traceback = excinfo.traceback - if self.tbfilter: - traceback = traceback.filter() - + traceback = excinfo.traceback + if self.tbfilter: + traceback = traceback.filter() + if isinstance(excinfo.value, RecursionError): - traceback, extraline = self._truncate_recursive_traceback(traceback) - else: - extraline = None - - last = traceback[-1] - entries = [] + traceback, extraline = self._truncate_recursive_traceback(traceback) + else: + extraline = None + + last = traceback[-1] + entries = [] if self.style == "value": reprentry = self.repr_traceback_entry(last, excinfo) entries.append(reprentry) return ReprTraceback(entries, None, style=self.style) - for index, entry in enumerate(traceback): - einfo = (last == entry) and excinfo or None - reprentry = self.repr_traceback_entry(entry, einfo) - entries.append(reprentry) - return ReprTraceback(entries, extraline, style=self.style) - + for index, entry in enumerate(traceback): + einfo = (last == entry) and excinfo or None + reprentry = self.repr_traceback_entry(entry, einfo) + entries.append(reprentry) + return ReprTraceback(entries, extraline, style=self.style) + def _truncate_recursive_traceback( self, traceback: Traceback ) -> Tuple[Traceback, Optional[str]]: """Truncate the given recursive traceback trying to find the starting point of the recursion. - + The detection is done by going through each traceback entry and 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 comparing numpy arrays using equality raises a TypeError), in which case we do our best to warn the user of the error and show a limited traceback. - """ - try: - recursionindex = traceback.recursionindex() - except Exception as e: - max_frames = 10 + """ + try: + recursionindex = traceback.recursionindex() + except Exception as e: + max_frames = 10 extraline: Optional[str] = ( - "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" - " The following exception happened when comparing locals in the stack frame:\n" - " {exc_type}: {exc_msg}\n" - " Displaying first and last {max_frames} stack frames out of {total}." - ).format( - exc_type=type(e).__name__, + "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" + " The following exception happened when comparing locals in the stack frame:\n" + " {exc_type}: {exc_msg}\n" + " Displaying first and last {max_frames} stack frames out of {total}." + ).format( + exc_type=type(e).__name__, exc_msg=str(e), - max_frames=max_frames, - total=len(traceback), + max_frames=max_frames, + total=len(traceback), ) # Type ignored because adding two instaces of a List subtype # currently incorrectly has type List instead of the subtype. traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore - else: - if recursionindex is not None: - extraline = "!!! Recursion detected (same locals & position)" - traceback = traceback[: recursionindex + 1] - else: - extraline = None - - return traceback, extraline - + else: + if recursionindex is not None: + extraline = "!!! Recursion detected (same locals & position)" + traceback = traceback[: recursionindex + 1] + else: + extraline = None + + return traceback, extraline + def repr_excinfo( self, excinfo: ExceptionInfo[BaseException] ) -> "ExceptionChainRepr": @@ -913,7 +913,7 @@ class FormattedExcinfo: traceback.format_exception(type(e), e, None) ) reprcrash = None - + repr_chain += [(reprtraceback, reprcrash, descr)] if e.__cause__ is not None and self.chain: e = e.__cause__ @@ -937,46 +937,46 @@ class FormattedExcinfo: e = None repr_chain.reverse() return ExceptionChainRepr(repr_chain) - - + + @attr.s(eq=False) class TerminalRepr: def __str__(self) -> str: - # FYI this is called from pytest-xdist's serialization of exception - # information. + # FYI this is called from pytest-xdist's serialization of exception + # information. io = StringIO() tw = TerminalWriter(file=io) - self.toterminal(tw) - return io.getvalue().strip() - + self.toterminal(tw) + return io.getvalue().strip() + def __repr__(self) -> str: return "<{} instance at {:0x}>".format(self.__class__, id(self)) - + def toterminal(self, tw: TerminalWriter) -> None: raise NotImplementedError() - + # This class is abstract -- only subclasses are instantiated. @attr.s(eq=False) -class ExceptionRepr(TerminalRepr): +class ExceptionRepr(TerminalRepr): # Provided by subclasses. reprcrash: Optional["ReprFileLocation"] reprtraceback: "ReprTraceback" - + def __attrs_post_init__(self) -> None: self.sections: List[Tuple[str, str, str]] = [] def addsection(self, name: str, content: str, sep: str = "-") -> None: - self.sections.append((name, content, sep)) - + self.sections.append((name, content, sep)) + def toterminal(self, tw: TerminalWriter) -> None: - for name, content, sep in self.sections: - tw.sep(sep, name) - tw.line(content) - - + for name, content, sep in self.sections: + tw.sep(sep, name) + tw.line(content) + + @attr.s(eq=False) -class ExceptionChainRepr(ExceptionRepr): +class ExceptionChainRepr(ExceptionRepr): chain = attr.ib( type=Sequence[ Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]] @@ -985,81 +985,81 @@ class ExceptionChainRepr(ExceptionRepr): def __attrs_post_init__(self) -> None: 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. self.reprtraceback = self.chain[-1][0] self.reprcrash = self.chain[-1][1] - + def toterminal(self, tw: TerminalWriter) -> None: - for element in self.chain: - element[0].toterminal(tw) - if element[2] is not None: - tw.line("") - tw.line(element[2], yellow=True) + for element in self.chain: + element[0].toterminal(tw) + if element[2] is not None: + tw.line("") + tw.line(element[2], yellow=True) super().toterminal(tw) - - + + @attr.s(eq=False) -class ReprExceptionInfo(ExceptionRepr): +class ReprExceptionInfo(ExceptionRepr): reprtraceback = attr.ib(type="ReprTraceback") reprcrash = attr.ib(type="ReprFileLocation") - + def toterminal(self, tw: TerminalWriter) -> None: - self.reprtraceback.toterminal(tw) + self.reprtraceback.toterminal(tw) super().toterminal(tw) - - + + @attr.s(eq=False) -class ReprTraceback(TerminalRepr): +class ReprTraceback(TerminalRepr): reprentries = attr.ib(type=Sequence[Union["ReprEntry", "ReprEntryNative"]]) extraline = attr.ib(type=Optional[str]) style = attr.ib(type="_TracebackStyle") - entrysep = "_ " - + entrysep = "_ " + def toterminal(self, tw: TerminalWriter) -> None: # The entries might have different styles. - for i, entry in enumerate(self.reprentries): - if entry.style == "long": - tw.line("") - entry.toterminal(tw) - if i < len(self.reprentries) - 1: - next_entry = self.reprentries[i + 1] - if ( - entry.style == "long" - or entry.style == "short" - and next_entry.style == "long" - ): - tw.sep(self.entrysep) - - if self.extraline: - tw.line(self.extraline) - - -class ReprTracebackNative(ReprTraceback): + for i, entry in enumerate(self.reprentries): + if entry.style == "long": + tw.line("") + entry.toterminal(tw) + if i < len(self.reprentries) - 1: + next_entry = self.reprentries[i + 1] + if ( + entry.style == "long" + or entry.style == "short" + and next_entry.style == "long" + ): + tw.sep(self.entrysep) + + if self.extraline: + tw.line(self.extraline) + + +class ReprTracebackNative(ReprTraceback): def __init__(self, tblines: Sequence[str]) -> None: - self.style = "native" - self.reprentries = [ReprEntryNative(tblines)] - self.extraline = None - - + self.style = "native" + self.reprentries = [ReprEntryNative(tblines)] + self.extraline = None + + @attr.s(eq=False) -class ReprEntryNative(TerminalRepr): +class ReprEntryNative(TerminalRepr): lines = attr.ib(type=Sequence[str]) style: "_TracebackStyle" = "native" - + def toterminal(self, tw: TerminalWriter) -> None: - tw.write("".join(self.lines)) - - + tw.write("".join(self.lines)) + + @attr.s(eq=False) -class ReprEntry(TerminalRepr): +class ReprEntry(TerminalRepr): lines = attr.ib(type=Sequence[str]) reprfuncargs = attr.ib(type=Optional["ReprFuncArgs"]) reprlocals = attr.ib(type=Optional["ReprLocals"]) reprfileloc = attr.ib(type=Optional["ReprFileLocation"]) style = attr.ib(type="_TracebackStyle") - + def _write_entry_lines(self, tw: TerminalWriter) -> None: """Write the source code portions of a list of traceback entries with syntax highlighting. @@ -1105,82 +1105,82 @@ class ReprEntry(TerminalRepr): tw.line(line, bold=True, red=True) def toterminal(self, tw: TerminalWriter) -> None: - if self.style == "short": + if self.style == "short": assert self.reprfileloc is not None - self.reprfileloc.toterminal(tw) + self.reprfileloc.toterminal(tw) self._write_entry_lines(tw) if self.reprlocals: self.reprlocals.toterminal(tw, indent=" " * 8) - return + return - if self.reprfuncargs: - self.reprfuncargs.toterminal(tw) + if self.reprfuncargs: + self.reprfuncargs.toterminal(tw) self._write_entry_lines(tw) - if self.reprlocals: - tw.line("") - self.reprlocals.toterminal(tw) - if self.reprfileloc: - if self.lines: - tw.line("") - self.reprfileloc.toterminal(tw) - + if self.reprlocals: + tw.line("") + self.reprlocals.toterminal(tw) + if self.reprfileloc: + if self.lines: + tw.line("") + self.reprfileloc.toterminal(tw) + def __str__(self) -> str: return "{}\n{}\n{}".format( "\n".join(self.lines), self.reprlocals, self.reprfileloc ) - - + + @attr.s(eq=False) -class ReprFileLocation(TerminalRepr): +class ReprFileLocation(TerminalRepr): path = attr.ib(type=str, converter=str) lineno = attr.ib(type=int) message = attr.ib(type=str) - + def toterminal(self, tw: TerminalWriter) -> None: # Filename and lineno output for each entry, using an output format # that most editors understand. - msg = self.message - i = msg.find("\n") - if i != -1: - msg = msg[:i] - tw.write(self.path, bold=True, red=True) + msg = self.message + i = msg.find("\n") + if i != -1: + msg = msg[:i] + tw.write(self.path, bold=True, red=True) tw.line(f":{self.lineno}: {msg}") - - + + @attr.s(eq=False) -class ReprLocals(TerminalRepr): +class ReprLocals(TerminalRepr): lines = attr.ib(type=Sequence[str]) - + def toterminal(self, tw: TerminalWriter, indent="") -> None: - for line in self.lines: + for line in self.lines: tw.line(indent + line) - - + + @attr.s(eq=False) -class ReprFuncArgs(TerminalRepr): +class ReprFuncArgs(TerminalRepr): args = attr.ib(type=Sequence[Tuple[str, object]]) - + def toterminal(self, tw: TerminalWriter) -> None: - if self.args: - linesofar = "" - for name, value in self.args: + if self.args: + linesofar = "" + for name, value in self.args: ns = f"{name} = {value}" - if len(ns) + len(linesofar) + 2 > tw.fullwidth: - if linesofar: - tw.line(linesofar) - linesofar = ns - else: - if linesofar: - linesofar += ", " + ns - else: - linesofar = ns - if linesofar: - tw.line(linesofar) - tw.line("") - - + if len(ns) + len(linesofar) + 2 > tw.fullwidth: + if linesofar: + tw.line(linesofar) + linesofar = ns + else: + if linesofar: + linesofar += ", " + ns + else: + linesofar = ns + if linesofar: + tw.line(linesofar) + tw.line("") + + def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: """Return source location (path, lineno) for the given object. @@ -1195,14 +1195,14 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: if hasattr(obj, "place_as"): obj = obj.place_as # type: ignore[attr-defined] - try: + try: code = Code.from_function(obj) except TypeError: try: fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type] except TypeError: return "", -1 - + fspath = fn and py.path.local(fn) or "" lineno = -1 if fspath: @@ -1211,40 +1211,40 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: except OSError: pass return fspath, lineno - + return code.path, code.firstlineno # Relative paths that we use to filter traceback entries from appearing to the user; # 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. - + _PLUGGY_DIR = Path(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 if _PLUGGY_DIR.name == "__init__.py": _PLUGGY_DIR = _PLUGGY_DIR.parent _PYTEST_DIR = Path(_pytest.__file__).parent _PY_DIR = Path(py.__file__).parent - - + + def filter_traceback(entry: TracebackEntry) -> bool: """Return True if a TracebackEntry instance should be included in tracebacks. We hide traceback entries of: - * dynamically generated code (no code to show up for it); - * internal traceback from pytest or its internal libraries, py and pluggy. - """ - # entry.path might sometimes return a str object when the entry + * dynamically generated code (no code to show up for it); + * internal traceback from pytest or its internal libraries, py and pluggy. + """ + # entry.path might sometimes return a str object when the entry # points to dynamically generated code. # See https://bitbucket.org/pytest-dev/py/issues/71. - raw_filename = entry.frame.code.raw.co_filename - is_generated = "<" in raw_filename and ">" in raw_filename - if is_generated: - return False + raw_filename = entry.frame.code.raw.co_filename + is_generated = "<" in raw_filename and ">" in raw_filename + if is_generated: + 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. p = Path(entry.path) diff --git a/contrib/python/pytest/py3/_pytest/_code/source.py b/contrib/python/pytest/py3/_pytest/_code/source.py index 56bf0fdc20..6f54057c0a 100644 --- a/contrib/python/pytest/py3/_pytest/_code/source.py +++ b/contrib/python/pytest/py3/_pytest/_code/source.py @@ -1,10 +1,10 @@ -import ast -import inspect -import textwrap -import tokenize +import ast +import inspect +import textwrap +import tokenize import types -import warnings -from bisect import bisect_right +import warnings +from bisect import bisect_right from typing import Iterable from typing import Iterator from typing import List @@ -12,14 +12,14 @@ from typing import Optional from typing import overload from typing import Tuple from typing import Union - - + + class Source: """An immutable object holding a source code fragment. - + When using Source(...), the source lines are deindented. - """ - + """ + def __init__(self, obj: object = None) -> None: if not obj: self.lines: List[str] = [] @@ -36,15 +36,15 @@ class Source: except TypeError: src = inspect.getsource(obj) # type: ignore[arg-type] self.lines = deindent(src.split("\n")) - + def __eq__(self, other: object) -> bool: if not isinstance(other, Source): return NotImplemented return self.lines == other.lines - + # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore - + @overload def __getitem__(self, key: int) -> str: ... @@ -54,81 +54,81 @@ class Source: ... def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: - if isinstance(key, int): - return self.lines[key] - else: - if key.step not in (None, 1): - raise IndexError("cannot slice a Source with a step") - newsource = Source() - newsource.lines = self.lines[key.start : key.stop] - return newsource - + if isinstance(key, int): + return self.lines[key] + else: + if key.step not in (None, 1): + raise IndexError("cannot slice a Source with a step") + newsource = Source() + newsource.lines = self.lines[key.start : key.stop] + return newsource + def __iter__(self) -> Iterator[str]: return iter(self.lines) def __len__(self) -> int: - return len(self.lines) - + return len(self.lines) + def strip(self) -> "Source": """Return new Source object with trailing and leading blank lines removed.""" - start, end = 0, len(self) - while start < end and not self.lines[start].strip(): - start += 1 - while end > start and not self.lines[end - 1].strip(): - end -= 1 - source = Source() - source.lines[:] = self.lines[start:end] - return source - + start, end = 0, len(self) + while start < end and not self.lines[start].strip(): + start += 1 + while end > start and not self.lines[end - 1].strip(): + end -= 1 + source = Source() + source.lines[:] = self.lines[start:end] + return source + def indent(self, indent: str = " " * 4) -> "Source": """Return a copy of the source object with all lines indented by the given indent-string.""" - newsource = Source() - newsource.lines = [(indent + line) for line in self.lines] - return newsource - + newsource = Source() + newsource.lines = [(indent + line) for line in self.lines] + return newsource + def getstatement(self, lineno: int) -> "Source": """Return Source statement which contains the given linenumber (counted from 0).""" - start, end = self.getstatementrange(lineno) - return self[start:end] - + start, end = self.getstatementrange(lineno) + return self[start:end] + def getstatementrange(self, lineno: int) -> Tuple[int, int]: """Return (start, end) tuple which spans the minimal statement region which containing the given lineno.""" - if not (0 <= lineno < len(self)): - raise IndexError("lineno out of range") - ast, start, end = getstatementrange_ast(lineno, self) - return start, end - + if not (0 <= lineno < len(self)): + raise IndexError("lineno out of range") + ast, start, end = getstatementrange_ast(lineno, self) + return start, end + def deindent(self) -> "Source": """Return a new Source object deindented.""" - newsource = Source() - newsource.lines[:] = deindent(self.lines) - return newsource - + newsource = Source() + newsource.lines[:] = deindent(self.lines) + return newsource + def __str__(self) -> str: - return "\n".join(self.lines) - - -# -# helper functions -# - - + return "\n".join(self.lines) + + +# +# helper functions +# + + def findsource(obj) -> Tuple[Optional[Source], int]: - try: - sourcelines, lineno = inspect.findsource(obj) + try: + sourcelines, lineno = inspect.findsource(obj) except Exception: - return None, -1 - source = Source() - source.lines = [line.rstrip() for line in sourcelines] - return source, lineno - - + return None, -1 + source = Source() + source.lines = [line.rstrip() for line in sourcelines] + return source, lineno + + def getrawcode(obj: object, trycall: bool = True) -> types.CodeType: """Return code object for given function.""" - try: + try: return obj.__code__ # type: ignore[attr-defined,no-any-return] except AttributeError: pass @@ -137,76 +137,76 @@ def getrawcode(obj: object, trycall: bool = True) -> types.CodeType: if call and not isinstance(obj, type): return getrawcode(call, trycall=False) raise TypeError(f"could not get code object for {obj!r}") - - + + def deindent(lines: Iterable[str]) -> List[str]: - return textwrap.dedent("\n".join(lines)).splitlines() - - + return textwrap.dedent("\n".join(lines)).splitlines() + + def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: # Flatten all statements and except handlers into one lineno-list. # AST's line numbers start indexing at 1. values: List[int] = [] - for x in ast.walk(node): - if isinstance(x, (ast.stmt, ast.ExceptHandler)): - values.append(x.lineno - 1) - for name in ("finalbody", "orelse"): + for x in ast.walk(node): + if isinstance(x, (ast.stmt, ast.ExceptHandler)): + values.append(x.lineno - 1) + for name in ("finalbody", "orelse"): val: Optional[List[ast.stmt]] = getattr(x, name, None) - if val: + if val: # Treat the finally/orelse part as its own statement. - values.append(val[0].lineno - 1 - 1) - values.sort() - insert_index = bisect_right(values, lineno) - start = values[insert_index - 1] - if insert_index >= len(values): - end = None - else: - end = values[insert_index] - return start, end - - + values.append(val[0].lineno - 1 - 1) + values.sort() + insert_index = bisect_right(values, lineno) + start = values[insert_index - 1] + if insert_index >= len(values): + end = None + else: + end = values[insert_index] + return start, end + + def getstatementrange_ast( lineno: int, source: Source, assertion: bool = False, astnode: Optional[ast.AST] = None, ) -> Tuple[ast.AST, int, int]: - if astnode is None: - content = str(source) - # See #4260: + if astnode is None: + content = str(source) + # See #4260: # Don't produce duplicate warnings when compiling source to find AST. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") 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: - # - ast-parsing strips comments - # - there might be empty lines - # - we might have lesser indented code blocks at the end - if end is None: - end = len(source.lines) - - if end > start + 1: + # - ast-parsing strips comments + # - there might be empty lines + # - we might have lesser indented code blocks at the end + if end is None: + end = len(source.lines) + + if end > start + 1: # Make sure we don't span differently indented code blocks # 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. - block_finder.started = source.lines[start][0].isspace() - it = ((x + "\n") for x in source.lines[start:end]) - try: - for tok in tokenize.generate_tokens(lambda: next(it)): - block_finder.tokeneater(*tok) - except (inspect.EndOfBlock, IndentationError): - end = block_finder.last + start - except Exception: - pass - + block_finder.started = source.lines[start][0].isspace() + it = ((x + "\n") for x in source.lines[start:end]) + try: + for tok in tokenize.generate_tokens(lambda: next(it)): + block_finder.tokeneater(*tok) + except (inspect.EndOfBlock, IndentationError): + end = block_finder.last + start + except Exception: + pass + # The end might still point to a comment or empty line, correct it. - while end: - line = source.lines[end - 1].lstrip() - if line.startswith("#") or not line: - end -= 1 - else: - break - return astnode, start, end + while end: + line = source.lines[end - 1].lstrip() + if line.startswith("#") or not line: + end -= 1 + else: + break + return astnode, start, end diff --git a/contrib/python/pytest/py3/_pytest/_version.py b/contrib/python/pytest/py3/_pytest/_version.py index 347a74fd9e..83518587e4 100644 --- a/contrib/python/pytest/py3/_pytest/_version.py +++ b/contrib/python/pytest/py3/_pytest/_version.py @@ -1,5 +1,5 @@ -# coding: utf-8 -# file generated by setuptools_scm -# don't change, don't track in version control +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control version = '6.2.5' version_tuple = (6, 2, 5) diff --git a/contrib/python/pytest/py3/_pytest/assertion/__init__.py b/contrib/python/pytest/py3/_pytest/assertion/__init__.py index d5d3be749a..a18cf198df 100644 --- a/contrib/python/pytest/py3/_pytest/assertion/__init__.py +++ b/contrib/python/pytest/py3/_pytest/assertion/__init__.py @@ -1,40 +1,40 @@ """Support for presenting detailed information in failing assertions.""" -import sys +import sys from typing import Any from typing import Generator from typing import List from typing import Optional from typing import TYPE_CHECKING - -from _pytest.assertion import rewrite -from _pytest.assertion import truncate -from _pytest.assertion import util + +from _pytest.assertion import rewrite +from _pytest.assertion import truncate +from _pytest.assertion import util from _pytest.assertion.rewrite import assertstate_key from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.nodes import Item - + if TYPE_CHECKING: from _pytest.main import Session - + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("debugconfig") - group.addoption( - "--assert", - action="store", - dest="assertmode", - choices=("rewrite", "plain"), - default="rewrite", - metavar="MODE", + group = parser.getgroup("debugconfig") + group.addoption( + "--assert", + action="store", + dest="assertmode", + choices=("rewrite", "plain"), + default="rewrite", + metavar="MODE", help=( "Control assertion debugging tools.\n" "'plain' performs no assertion debugging.\n" "'rewrite' (the default) rewrites assert statements in test modules" " on import to provide assert expression information." ), - ) + ) parser.addini( "enable_assertion_pass_hook", type="bool", @@ -42,125 +42,125 @@ def pytest_addoption(parser: Parser) -> None: help="Enables the pytest_assertion_pass hook." "Make sure to delete any previously generated pyc cache files.", ) - - + + def register_assert_rewrite(*names: str) -> None: - """Register one or more module names to be rewritten on import. - - This function will make sure that this module or all modules inside - the package will get their assert statements rewritten. - Thus you should make sure to call this before the module is - actually imported, usually in your __init__.py if you are a plugin - using a package. - + """Register one or more module names to be rewritten on import. + + This function will make sure that this module or all modules inside + the package will get their assert statements rewritten. + Thus you should make sure to call this before the module is + actually imported, usually in your __init__.py if you are a plugin + using a package. + :raises TypeError: If the given module names are not strings. - """ - for name in names: - if not isinstance(name, str): + """ + for name in names: + if not isinstance(name, str): msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable] - raise TypeError(msg.format(repr(names))) - for hook in sys.meta_path: - if isinstance(hook, rewrite.AssertionRewritingHook): - importhook = hook - break - else: + raise TypeError(msg.format(repr(names))) + for hook in sys.meta_path: + if isinstance(hook, rewrite.AssertionRewritingHook): + importhook = hook + break + else: # TODO(typing): Add a protocol for mark_rewrite() and use it # for importhook and for PytestPluginManager.rewrite_hook. importhook = DummyRewriteHook() # type: ignore - importhook.mark_rewrite(*names) - - + importhook.mark_rewrite(*names) + + class DummyRewriteHook: - """A no-op import hook for when rewriting is disabled.""" - + """A no-op import hook for when rewriting is disabled.""" + def mark_rewrite(self, *names: str) -> None: - pass - - + pass + + class AssertionState: - """State for the assertion plugin.""" - + """State for the assertion plugin.""" + def __init__(self, config: Config, mode) -> None: - self.mode = mode - self.trace = config.trace.root.get("assertion") + self.mode = mode + self.trace = config.trace.root.get("assertion") self.hook: Optional[rewrite.AssertionRewritingHook] = None - - + + def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: - """Try to install the rewrite hook, raise SystemError if it fails.""" + """Try to install the rewrite hook, raise SystemError if it fails.""" config._store[assertstate_key] = AssertionState(config, "rewrite") config._store[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config) - sys.meta_path.insert(0, hook) + sys.meta_path.insert(0, hook) config._store[assertstate_key].trace("installed rewrite import hook") - + def undo() -> None: hook = config._store[assertstate_key].hook - if hook is not None and hook in sys.meta_path: - sys.meta_path.remove(hook) - - config.add_cleanup(undo) - return hook - - + if hook is not None and hook in sys.meta_path: + sys.meta_path.remove(hook) + + config.add_cleanup(undo) + return hook + + def pytest_collection(session: "Session") -> None: # 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). assertstate = session.config._store.get(assertstate_key, None) - if assertstate: - if assertstate.hook is not None: - assertstate.hook.set_session(session) - - + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(session) + + @hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. - + The rewrite module will use util._reprcompare if it exists to use custom reporting via the pytest_assertrepr_compare hook. This sets up this custom - comparison for the test. - """ - + comparison for the test. + """ + ihook = item.ihook - + def callbinrepr(op, left: object, right: object) -> Optional[str]: """Call the pytest_assertrepr_compare hook and prepare the result. - This uses the first result from the hook and then ensures the - following: - * Overly verbose explanations are truncated unless configured otherwise - (eg. if running in verbose mode). - * Embedded newlines are escaped to help util.format_explanation() - later. - * If the rewrite mode is used embedded %-characters are replaced - to protect later % formatting. - - The result can be formatted by util.format_explanation() for - pretty printing. - """ + This uses the first result from the hook and then ensures the + following: + * Overly verbose explanations are truncated unless configured otherwise + (eg. if running in verbose mode). + * Embedded newlines are escaped to help util.format_explanation() + later. + * If the rewrite mode is used embedded %-characters are replaced + to protect later % formatting. + + The result can be formatted by util.format_explanation() for + pretty printing. + """ hook_result = ihook.pytest_assertrepr_compare( - config=item.config, op=op, left=left, right=right - ) - for new_expl in hook_result: - if new_expl: - new_expl = truncate.truncate_if_required(new_expl, item) - new_expl = [line.replace("\n", "\\n") for line in new_expl] + config=item.config, op=op, left=left, right=right + ) + for new_expl in hook_result: + if new_expl: + new_expl = truncate.truncate_if_required(new_expl, item) + new_expl = [line.replace("\n", "\\n") for line in new_expl] res = "\n~".join(new_expl) - if item.config.getvalue("assertmode") == "rewrite": - res = res.replace("%", "%%") - return res + if item.config.getvalue("assertmode") == "rewrite": + res = res.replace("%", "%%") + return res return None - + saved_assert_hooks = util._reprcompare, util._assertion_pass - util._reprcompare = callbinrepr - + util._reprcompare = callbinrepr + if ihook.pytest_assertion_pass.get_hookimpls(): - + def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl) - + util._assertion_pass = call_assertion_pass_hook - + yield util._reprcompare, util._assertion_pass = saved_assert_hooks @@ -168,11 +168,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_sessionfinish(session: "Session") -> None: assertstate = session.config._store.get(assertstate_key, None) - if assertstate: - if assertstate.hook is not None: - assertstate.hook.set_session(None) - - + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(None) + + def pytest_assertrepr_compare( config: Config, op: str, left: Any, right: Any ) -> Optional[List[str]]: diff --git a/contrib/python/pytest/py3/_pytest/assertion/rewrite.py b/contrib/python/pytest/py3/_pytest/assertion/rewrite.py index 1596998c8c..37ff076aab 100644 --- a/contrib/python/pytest/py3/_pytest/assertion/rewrite.py +++ b/contrib/python/pytest/py3/_pytest/assertion/rewrite.py @@ -1,18 +1,18 @@ """Rewrite assertion AST to produce nice error messages.""" -import ast -import errno +import ast +import errno import functools import importlib.abc import importlib.machinery import importlib.util import io -import itertools -import marshal -import os -import struct -import sys +import itertools +import marshal +import os +import struct +import sys import tokenize -import types +import types from pathlib import Path from pathlib import PurePath from typing import Callable @@ -26,38 +26,38 @@ from typing import Set from typing import Tuple from typing import TYPE_CHECKING from typing import Union - + import py from _pytest._io.saferepr import saferepr from _pytest._version import version -from _pytest.assertion import util +from _pytest.assertion import util from _pytest.assertion.util import ( # noqa: F401 format_explanation as _format_explanation, ) from _pytest.config import Config from _pytest.main import Session -from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import fnmatch_ex from _pytest.store import StoreKey - + if TYPE_CHECKING: from _pytest.assertion import AssertionState - - + + assertstate_key = StoreKey["AssertionState"]() - - + + # pytest caches rewritten pycs in pycache dirs PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT - - + + class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): """PEP302/PEP451 import hook which rewrites asserts.""" - + def __init__(self, config: Config) -> None: - self.config = config + self.config = config try: self.fnpats = config.getini("python_files") except ValueError: @@ -65,32 +65,32 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) self.session: Optional[Session] = None self._rewritten_names: Set[str] = set() self._must_rewrite: Set[str] = set() - # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, - # which might result in infinite recursion (#3506) - self._writing_pyc = False - self._basenames_to_check_rewrite = {"conftest"} + # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, + # which might result in infinite recursion (#3506) + self._writing_pyc = False + self._basenames_to_check_rewrite = {"conftest"} self._marked_for_rewrite_cache: Dict[str, bool] = {} - self._session_paths_checked = False - + self._session_paths_checked = False + def set_session(self, session: Optional[Session]) -> None: - self.session = session - self._session_paths_checked = False - + self.session = session + self._session_paths_checked = False + # Indirection so we can mock calls to find_spec originated from the hook during testing _find_spec = importlib.machinery.PathFinder.find_spec - + def find_spec( self, name: str, path: Optional[Sequence[Union[str, bytes]]] = None, target: Optional[types.ModuleType] = None, ) -> Optional[importlib.machinery.ModuleSpec]: - if self._writing_pyc: - return None + if self._writing_pyc: + return None state = self.config._store[assertstate_key] - if self._early_rewrite_bailout(name, state): - return None - state.trace("find_module called for: %s" % name) + if self._early_rewrite_bailout(name, state): + return None + state.trace("find_module called for: %s" % name) # Type ignored because mypy is confused about the `self` binding here. spec = self._find_spec(name, path) # type: ignore @@ -109,19 +109,19 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) or not os.path.exists(spec.origin) ): return None - else: + else: fn = spec.origin - + if not self._should_rewrite(name, fn, state): - return None - + return None + return importlib.util.spec_from_file_location( name, fn, loader=self, submodule_search_locations=spec.submodule_search_locations, ) - + def create_module( self, spec: importlib.machinery.ModuleSpec ) -> Optional[types.ModuleType]: @@ -135,17 +135,17 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) self._rewritten_names.add(module.__name__) - # The requested module looks like a test file, so rewrite it. This is - # the most magical part of the process: load the source, rewrite the - # asserts, and load the rewritten source. We also cache the rewritten - # module code in a special pyc. We must be aware of the possibility of - # concurrent pytest processes rewriting and loading pycs. To avoid - # tricky race conditions, we maintain the following invariant: The - # cached pyc is always a complete, valid pyc. Operations on it must be - # atomic. POSIX's atomic rename comes in handy. - write = not sys.dont_write_bytecode + # The requested module looks like a test file, so rewrite it. This is + # the most magical part of the process: load the source, rewrite the + # asserts, and load the rewritten source. We also cache the rewritten + # module code in a special pyc. We must be aware of the possibility of + # concurrent pytest processes rewriting and loading pycs. To avoid + # tricky race conditions, we maintain the following invariant: The + # cached pyc is always a complete, valid pyc. Operations on it must be + # atomic. POSIX's atomic rename comes in handy. + write = not sys.dont_write_bytecode cache_dir = get_cache_dir(fn) - if write: + if write: ok = try_makedirs(cache_dir) if not ok: write = False @@ -153,22 +153,22 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) cache_name = fn.name[:-3] + PYC_TAIL pyc = cache_dir / cache_name - # Notice that even if we're in a read-only directory, I'm going - # to check for a cached pyc. This may not be optimal... + # Notice that even if we're in a read-only directory, I'm going + # to check for a cached pyc. This may not be optimal... co = _read_pyc(fn, pyc, state.trace) - if co is None: + if co is None: state.trace(f"rewriting {fn!r}") source_stat, co = _rewrite_test(fn, self.config) - if write: - self._writing_pyc = True - try: - _write_pyc(state, co, source_stat, pyc) - finally: - self._writing_pyc = False - else: + if write: + self._writing_pyc = True + try: + _write_pyc(state, co, source_stat, pyc) + finally: + self._writing_pyc = False + else: state.trace(f"found cached rewritten pyc for {fn}") exec(co, module.__dict__) - + def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: """A fast way to get out of rewriting modules. @@ -176,111 +176,111 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) the find_spec from this class) is a major slowdown, so, this method tries to filter what we're sure won't be rewritten before getting to it. - """ - if self.session is not None and not self._session_paths_checked: - self._session_paths_checked = True + """ + if self.session is not None and not self._session_paths_checked: + self._session_paths_checked = True for initial_path in self.session._initialpaths: - # Make something as c:/projects/my_project/path.py -> - # ['c:', 'projects', 'my_project', 'path.py'] + # Make something as c:/projects/my_project/path.py -> + # ['c:', 'projects', 'my_project', 'path.py'] parts = str(initial_path).split(os.path.sep) - # add 'path' to basenames to be checked. - self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) - - # Note: conftest already by default in _basenames_to_check_rewrite. - parts = name.split(".") - if parts[-1] in self._basenames_to_check_rewrite: - return False - - # For matching the name it must be as if it was a filename. - path = PurePath(os.path.sep.join(parts) + ".py") - - for pat in self.fnpats: - # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based - # on the name alone because we need to match against the full path - if os.path.dirname(pat): - return False - if fnmatch_ex(pat, path): - return False - - if self._is_marked_for_rewrite(name, state): - return False - + # add 'path' to basenames to be checked. + self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) + + # Note: conftest already by default in _basenames_to_check_rewrite. + parts = name.split(".") + if parts[-1] in self._basenames_to_check_rewrite: + return False + + # For matching the name it must be as if it was a filename. + path = PurePath(os.path.sep.join(parts) + ".py") + + for pat in self.fnpats: + # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based + # on the name alone because we need to match against the full path + if os.path.dirname(pat): + return False + if fnmatch_ex(pat, path): + return False + + if self._is_marked_for_rewrite(name, state): + return False + state.trace(f"early skip of rewriting module: {name}") - return True - + return True + def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: - # always rewrite conftest files + # always rewrite conftest files if os.path.basename(fn) == "conftest.py": state.trace(f"rewriting conftest file: {fn!r}") - return True - - if self.session is not None: + return True + + if self.session is not None: if self.session.isinitpath(py.path.local(fn)): state.trace(f"matched test file (was specified on cmdline): {fn!r}") - return True - - # modules not passed explicitly on the command line are only - # rewritten if they match the naming convention for test files + return True + + # modules not passed explicitly on the command line are only + # rewritten if they match the naming convention for test files fn_path = PurePath(fn) - for pat in self.fnpats: + for pat in self.fnpats: if fnmatch_ex(pat, fn_path): state.trace(f"matched test file {fn!r}") - return True - - return self._is_marked_for_rewrite(name, state) - + return True + + return self._is_marked_for_rewrite(name, state) + def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool: - try: - return self._marked_for_rewrite_cache[name] - except KeyError: - for marked in self._must_rewrite: - if name == marked or name.startswith(marked + "."): + try: + return self._marked_for_rewrite_cache[name] + except KeyError: + for marked in self._must_rewrite: + if name == marked or name.startswith(marked + "."): state.trace(f"matched marked file {name!r} (from {marked!r})") - self._marked_for_rewrite_cache[name] = True - return True - - self._marked_for_rewrite_cache[name] = False - return False - + self._marked_for_rewrite_cache[name] = True + return True + + self._marked_for_rewrite_cache[name] = False + return False + def mark_rewrite(self, *names: str) -> None: - """Mark import names as needing to be rewritten. - - The named module or package as well as any nested modules will - be rewritten on import. - """ - already_imported = ( - set(names).intersection(sys.modules).difference(self._rewritten_names) - ) - for name in already_imported: + """Mark import names as needing to be rewritten. + + The named module or package as well as any nested modules will + be rewritten on import. + """ + already_imported = ( + set(names).intersection(sys.modules).difference(self._rewritten_names) + ) + for name in already_imported: mod = sys.modules[name] - if not AssertionRewriter.is_rewrite_disabled( + if not AssertionRewriter.is_rewrite_disabled( mod.__doc__ or "" ) and not isinstance(mod.__loader__, type(self)): - self._warn_already_imported(name) - self._must_rewrite.update(names) - self._marked_for_rewrite_cache.clear() - + self._warn_already_imported(name) + self._must_rewrite.update(names) + self._marked_for_rewrite_cache.clear() + def _warn_already_imported(self, name: str) -> None: from _pytest.warning_types import PytestAssertRewriteWarning - + self.config.issue_config_time_warning( PytestAssertRewriteWarning( "Module already imported so cannot be rewritten: %s" % name ), - stacklevel=5, - ) - + stacklevel=5, + ) + def get_data(self, pathname: Union[str, bytes]) -> bytes: """Optional PEP302 get_data API.""" - with open(pathname, "rb") as f: - return f.read() - - + with open(pathname, "rb") as f: + return f.read() + + def _write_pyc_fp( fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType ) -> None: - # Technically, we don't have to have the same pyc format as - # (C)Python, since these "pycs" should never be seen by builtin + # Technically, we don't have to have the same pyc format as + # (C)Python, since these "pycs" should never be seen by builtin # import. However, there's little reason to deviate. fp.write(importlib.util.MAGIC_NUMBER) # https://www.python.org/dev/peps/pep-0552/ @@ -293,11 +293,11 @@ def _write_pyc_fp( # "<LL" stands for 2 unsigned longs, little-endian. fp.write(struct.pack("<LL", mtime, size)) fp.write(marshal.dumps(co)) - - + + if sys.platform == "win32": from atomicwrites import atomic_write - + def _write_pyc( state: "AssertionState", co: types.CodeType, @@ -314,8 +314,8 @@ if sys.platform == "win32": # file etc. return False return True - - + + else: def _write_pyc( @@ -354,35 +354,35 @@ def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeT tree = ast.parse(source, filename=fn_) rewrite_asserts(tree, source, fn_, config) co = compile(tree, fn_, "exec", dont_inherit=True) - return stat, co - - + return stat, co + + def _read_pyc( source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None ) -> Optional[types.CodeType]: - """Possibly read a pytest pyc containing rewritten code. - - Return rewritten code if successful or None if not. - """ - try: + """Possibly read a pytest pyc containing rewritten code. + + Return rewritten code if successful or None if not. + """ + try: fp = open(os.fspath(pyc), "rb") except OSError: - return None - with fp: + return None + with fp: # https://www.python.org/dev/peps/pep-0552/ has_flags = sys.version_info >= (3, 7) - try: + try: stat_result = os.stat(os.fspath(source)) mtime = int(stat_result.st_mtime) size = stat_result.st_size data = fp.read(16 if has_flags else 12) except OSError as e: trace(f"_read_pyc({source}): OSError {e}") - return None - # Check for invalid or out of date pyc file. + return None + # Check for invalid or out of date pyc file. if len(data) != (16 if has_flags else 12): trace("_read_pyc(%s): invalid pyc (too short)" % source) - return None + return None if data[:4] != importlib.util.MAGIC_NUMBER: trace("_read_pyc(%s): invalid pyc (bad magic number)" % source) return None @@ -397,101 +397,101 @@ def _read_pyc( if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF: trace("_read_pyc(%s): invalid pyc (incorrect size)" % source) return None - try: - co = marshal.load(fp) - except Exception as e: + try: + co = marshal.load(fp) + except Exception as e: trace(f"_read_pyc({source}): marshal.load error {e}") - return None - if not isinstance(co, types.CodeType): - trace("_read_pyc(%s): not a code object" % source) - return None - return co - - + return None + if not isinstance(co, types.CodeType): + trace("_read_pyc(%s): not a code object" % source) + return None + return co + + def rewrite_asserts( mod: ast.Module, source: bytes, module_path: Optional[str] = None, config: Optional[Config] = None, ) -> None: - """Rewrite the assert statements in mod.""" + """Rewrite the assert statements in mod.""" AssertionRewriter(module_path, config, source).run(mod) - - + + def _saferepr(obj: object) -> str: r"""Get a safe repr of an object for assertion error messages. - - The assertion formatting (util.format_explanation()) requires - newlines to be escaped since they are a special character for it. - Normally assertion.util.format_explanation() does this but for a - custom repr it is possible to contain one of the special escape - sequences, especially '\n{' and '\n}' are likely to be present in - JSON reprs. - """ + + The assertion formatting (util.format_explanation()) requires + newlines to be escaped since they are a special character for it. + Normally assertion.util.format_explanation() does this but for a + custom repr it is possible to contain one of the special escape + sequences, especially '\n{' and '\n}' are likely to be present in + JSON reprs. + """ return saferepr(obj).replace("\n", "\\n") - - + + def _format_assertmsg(obj: object) -> str: r"""Format the custom assertion message given. - - For strings this simply replaces newlines with '\n~' so that - util.format_explanation() will preserve them instead of escaping + + For strings this simply replaces newlines with '\n~' so that + util.format_explanation() will preserve them instead of escaping newlines. For other objects saferepr() is used first. - """ - # reprlib appears to have a bug which means that if a string - # contains a newline it gets escaped, however if an object has a - # .__repr__() which contains newlines it does not get escaped. - # However in either case we want to preserve the newline. + """ + # reprlib appears to have a bug which means that if a string + # contains a newline it gets escaped, however if an object has a + # .__repr__() which contains newlines it does not get escaped. + # However in either case we want to preserve the newline. replaces = [("\n", "\n~"), ("%", "%%")] if not isinstance(obj, str): obj = saferepr(obj) replaces.append(("\\n", "\n~")) - - for r1, r2 in replaces: - obj = obj.replace(r1, r2) - - return obj - - + + for r1, r2 in replaces: + obj = obj.replace(r1, r2) + + return obj + + def _should_repr_global_name(obj: object) -> bool: if callable(obj): return False - + try: return not hasattr(obj, "__name__") except Exception: return True - + def _format_boolop(explanations: Iterable[str], is_or: bool) -> str: - explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" + explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" return explanation.replace("%", "%%") - - + + def _call_reprcompare( ops: Sequence[str], results: Sequence[bool], expls: Sequence[str], each_obj: Sequence[object], ) -> str: - for i, res, expl in zip(range(len(ops)), results, expls): - try: - done = not res - except Exception: - done = True - if done: - break - if util._reprcompare is not None: - custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) - if custom is not None: - return custom - return expl - - + for i, res, expl in zip(range(len(ops)), results, expls): + try: + done = not res + except Exception: + done = True + if done: + break + if util._reprcompare is not None: + custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1]) + if custom is not None: + return custom + return expl + + def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None: if util._assertion_pass is not None: util._assertion_pass(lineno, orig, expl) - + def _check_if_assertion_pass_impl() -> bool: """Check if any plugins implement the pytest_assertion_pass hook @@ -502,47 +502,47 @@ def _check_if_assertion_pass_impl() -> bool: UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"} BINOP_MAP = { - ast.BitOr: "|", - ast.BitXor: "^", - ast.BitAnd: "&", - ast.LShift: "<<", - ast.RShift: ">>", - ast.Add: "+", - ast.Sub: "-", - ast.Mult: "*", - ast.Div: "/", - ast.FloorDiv: "//", - ast.Mod: "%%", # escaped for string formatting - ast.Eq: "==", - ast.NotEq: "!=", - ast.Lt: "<", - ast.LtE: "<=", - ast.Gt: ">", - ast.GtE: ">=", - ast.Pow: "**", - ast.Is: "is", - ast.IsNot: "is not", - ast.In: "in", - ast.NotIn: "not in", + ast.BitOr: "|", + ast.BitXor: "^", + ast.BitAnd: "&", + ast.LShift: "<<", + ast.RShift: ">>", + ast.Add: "+", + ast.Sub: "-", + ast.Mult: "*", + ast.Div: "/", + ast.FloorDiv: "//", + ast.Mod: "%%", # escaped for string formatting + ast.Eq: "==", + ast.NotEq: "!=", + ast.Lt: "<", + ast.LtE: "<=", + ast.Gt: ">", + ast.GtE: ">=", + ast.Pow: "**", + ast.Is: "is", + ast.IsNot: "is not", + ast.In: "in", + ast.NotIn: "not in", ast.MatMult: "@", -} - - -def set_location(node, lineno, col_offset): - """Set node location information recursively.""" - - def _fix(node, lineno, col_offset): - if "lineno" in node._attributes: - node.lineno = lineno - if "col_offset" in node._attributes: - node.col_offset = col_offset - for child in ast.iter_child_nodes(node): - _fix(child, lineno, col_offset) - - _fix(node, lineno, col_offset) - return node - - +} + + +def set_location(node, lineno, col_offset): + """Set node location information recursively.""" + + def _fix(node, lineno, col_offset): + if "lineno" in node._attributes: + node.lineno = lineno + if "col_offset" in node._attributes: + node.col_offset = col_offset + for child in ast.iter_child_nodes(node): + _fix(child, lineno, col_offset) + + _fix(node, lineno, col_offset) + return node + + def _get_assertion_exprs(src: bytes) -> Dict[int, str]: """Return a mapping from {lineno: "assertion test expression"}.""" ret: Dict[int, str] = {} @@ -597,65 +597,65 @@ def _get_assertion_exprs(src: bytes) -> Dict[int, str]: return ret -class AssertionRewriter(ast.NodeVisitor): - """Assertion rewriting implementation. - - The main entrypoint is to call .run() with an ast.Module instance, - this will then find all the assert statements and rewrite them to - provide intermediate values and a detailed assertion error. See - http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html - for an overview of how this works. - - The entry point here is .run() which will iterate over all the - statements in an ast.Module and for each ast.Assert statement it - finds call .visit() with it. Then .visit_Assert() takes over and - is responsible for creating new ast statements to replace the - original assert statement: it rewrites the test of an assertion - to provide intermediate values and replace it with an if statement - which raises an assertion error with a detailed explanation in +class AssertionRewriter(ast.NodeVisitor): + """Assertion rewriting implementation. + + The main entrypoint is to call .run() with an ast.Module instance, + this will then find all the assert statements and rewrite them to + provide intermediate values and a detailed assertion error. See + http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html + for an overview of how this works. + + The entry point here is .run() which will iterate over all the + statements in an ast.Module and for each ast.Assert statement it + finds call .visit() with it. Then .visit_Assert() takes over and + is responsible for creating new ast statements to replace the + original assert statement: it rewrites the test of an assertion + to provide intermediate values and replace it with an if statement + which raises an assertion error with a detailed explanation in case the expression is false and calls pytest_assertion_pass hook if expression is true. - - For this .visit_Assert() uses the visitor pattern to visit all the - AST nodes of the ast.Assert.test field, each visit call returning - an AST node and the corresponding explanation string. During this - state is kept in several instance attributes: - - :statements: All the AST statements which will replace the assert - statement. - - :variables: This is populated by .variable() with each variable - used by the statements so that they can all be set to None at - the end of the statements. - - :variable_counter: Counter to create new unique variables needed - by statements. Variables are created using .variable() and - have the form of "@py_assert0". - + + For this .visit_Assert() uses the visitor pattern to visit all the + AST nodes of the ast.Assert.test field, each visit call returning + an AST node and the corresponding explanation string. During this + state is kept in several instance attributes: + + :statements: All the AST statements which will replace the assert + statement. + + :variables: This is populated by .variable() with each variable + used by the statements so that they can all be set to None at + the end of the statements. + + :variable_counter: Counter to create new unique variables needed + by statements. Variables are created using .variable() and + have the form of "@py_assert0". + :expl_stmts: The AST statements which will be executed to get data from the assertion. This is the code which will construct the detailed assertion message that is used in the AssertionError or for the pytest_assertion_pass hook. - - :explanation_specifiers: A dict filled by .explanation_param() - with %-formatting placeholders and their corresponding - expressions to use in the building of an assertion message. - This is used by .pop_format_context() to build a message. - - :stack: A stack of the explanation_specifiers dicts maintained by - .push_format_context() and .pop_format_context() which allows - to build another %-formatted string while already building one. - - This state is reset on every new assert statement visited and used - by the other visitors. - """ - + + :explanation_specifiers: A dict filled by .explanation_param() + with %-formatting placeholders and their corresponding + expressions to use in the building of an assertion message. + This is used by .pop_format_context() to build a message. + + :stack: A stack of the explanation_specifiers dicts maintained by + .push_format_context() and .pop_format_context() which allows + to build another %-formatted string while already building one. + + This state is reset on every new assert statement visited and used + by the other visitors. + """ + def __init__( self, module_path: Optional[str], config: Optional[Config], source: bytes ) -> None: super().__init__() - self.module_path = module_path - self.config = config + self.module_path = module_path + self.config = config if config is not None: self.enable_assertion_pass_hook = config.getini( "enable_assertion_pass_hook" @@ -663,50 +663,50 @@ class AssertionRewriter(ast.NodeVisitor): else: self.enable_assertion_pass_hook = False self.source = source - + @functools.lru_cache(maxsize=1) def _assert_expr_to_lineno(self) -> Dict[int, str]: return _get_assertion_exprs(self.source) def run(self, mod: ast.Module) -> None: - """Find all assert statements in *mod* and rewrite them.""" - if not mod.body: - # Nothing to do. - return + """Find all assert statements in *mod* and rewrite them.""" + if not mod.body: + # Nothing to do. + return # We'll insert some special imports at the top of the module, but after any # docstrings and __future__ imports, so first figure out where that is. - doc = getattr(mod, "docstring", None) - expect_docstring = doc is None - if doc is not None and self.is_rewrite_disabled(doc): - return - pos = 0 - lineno = 1 - for item in mod.body: - if ( - expect_docstring - and isinstance(item, ast.Expr) - and isinstance(item.value, ast.Str) - ): - doc = item.value.s - if self.is_rewrite_disabled(doc): - return - expect_docstring = False - elif ( + doc = getattr(mod, "docstring", None) + expect_docstring = doc is None + if doc is not None and self.is_rewrite_disabled(doc): + return + pos = 0 + lineno = 1 + for item in mod.body: + if ( + expect_docstring + and isinstance(item, ast.Expr) + and isinstance(item.value, ast.Str) + ): + doc = item.value.s + if self.is_rewrite_disabled(doc): + return + expect_docstring = False + elif ( isinstance(item, ast.ImportFrom) and item.level == 0 and item.module == "__future__" - ): + ): pass else: - break - pos += 1 + break + pos += 1 # Special case: for a decorated function, set the lineno to that of the # first decorator, not the `def`. Issue #4984. if isinstance(item, ast.FunctionDef) and item.decorator_list: lineno = item.decorator_list[0].lineno - else: - lineno = item.lineno + else: + lineno = item.lineno # Now actually insert the special imports. if sys.version_info >= (3, 10): aliases = [ @@ -723,153 +723,153 @@ class AssertionRewriter(ast.NodeVisitor): ast.alias("builtins", "@py_builtins"), ast.alias("_pytest.assertion.rewrite", "@pytest_ar"), ] - imports = [ - ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases - ] - mod.body[pos:pos] = imports + imports = [ + ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases + ] + mod.body[pos:pos] = imports - # Collect asserts. + # Collect asserts. nodes: List[ast.AST] = [mod] - while nodes: - node = nodes.pop() - for name, field in ast.iter_fields(node): - if isinstance(field, list): + while nodes: + node = nodes.pop() + for name, field in ast.iter_fields(node): + if isinstance(field, list): new: List[ast.AST] = [] - for i, child in enumerate(field): - if isinstance(child, ast.Assert): - # Transform assert. - new.extend(self.visit(child)) - else: - new.append(child) - if isinstance(child, ast.AST): - nodes.append(child) - setattr(node, name, new) - elif ( - isinstance(field, ast.AST) - # Don't recurse into expressions as they can't contain - # asserts. - and not isinstance(field, ast.expr) - ): - nodes.append(field) - - @staticmethod + for i, child in enumerate(field): + if isinstance(child, ast.Assert): + # Transform assert. + new.extend(self.visit(child)) + else: + new.append(child) + if isinstance(child, ast.AST): + nodes.append(child) + setattr(node, name, new) + elif ( + isinstance(field, ast.AST) + # Don't recurse into expressions as they can't contain + # asserts. + and not isinstance(field, ast.expr) + ): + nodes.append(field) + + @staticmethod def is_rewrite_disabled(docstring: str) -> bool: - return "PYTEST_DONT_REWRITE" in docstring - + return "PYTEST_DONT_REWRITE" in docstring + def variable(self) -> str: - """Get a new variable.""" - # Use a character invalid in python identifiers to avoid clashing. - name = "@py_assert" + str(next(self.variable_counter)) - self.variables.append(name) - return name - + """Get a new variable.""" + # Use a character invalid in python identifiers to avoid clashing. + name = "@py_assert" + str(next(self.variable_counter)) + self.variables.append(name) + return name + def assign(self, expr: ast.expr) -> ast.Name: - """Give *expr* a name.""" - name = self.variable() - self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) - return ast.Name(name, ast.Load()) - + """Give *expr* a name.""" + name = self.variable() + self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) + return ast.Name(name, ast.Load()) + def display(self, expr: ast.expr) -> ast.expr: """Call saferepr on the expression.""" return self.helper("_saferepr", expr) - + def helper(self, name: str, *args: ast.expr) -> ast.expr: - """Call a helper in this module.""" - py_name = ast.Name("@pytest_ar", ast.Load()) + """Call a helper in this module.""" + py_name = ast.Name("@pytest_ar", ast.Load()) attr = ast.Attribute(py_name, name, ast.Load()) return ast.Call(attr, list(args), []) - + def builtin(self, name: str) -> ast.Attribute: - """Return the builtin called *name*.""" - builtin_name = ast.Name("@py_builtins", ast.Load()) - return ast.Attribute(builtin_name, name, ast.Load()) - + """Return the builtin called *name*.""" + builtin_name = ast.Name("@py_builtins", ast.Load()) + return ast.Attribute(builtin_name, name, ast.Load()) + def explanation_param(self, expr: ast.expr) -> str: - """Return a new named %-formatting placeholder for expr. - - This creates a %-formatting placeholder for expr in the - current formatting context, e.g. ``%(py0)s``. The placeholder - and expr are placed in the current format context so that it - can be used on the next call to .pop_format_context(). - """ - specifier = "py" + str(next(self.variable_counter)) - self.explanation_specifiers[specifier] = expr - return "%(" + specifier + ")s" - + """Return a new named %-formatting placeholder for expr. + + This creates a %-formatting placeholder for expr in the + current formatting context, e.g. ``%(py0)s``. The placeholder + and expr are placed in the current format context so that it + can be used on the next call to .pop_format_context(). + """ + specifier = "py" + str(next(self.variable_counter)) + self.explanation_specifiers[specifier] = expr + return "%(" + specifier + ")s" + def push_format_context(self) -> None: - """Create a new formatting context. - - The format context is used for when an explanation wants to - have a variable value formatted in the assertion message. In - this case the value required can be added using - .explanation_param(). Finally .pop_format_context() is used - to format a string of %-formatted values as added by - .explanation_param(). - """ + """Create a new formatting context. + + The format context is used for when an explanation wants to + have a variable value formatted in the assertion message. In + this case the value required can be added using + .explanation_param(). Finally .pop_format_context() is used + to format a string of %-formatted values as added by + .explanation_param(). + """ self.explanation_specifiers: Dict[str, ast.expr] = {} - self.stack.append(self.explanation_specifiers) - + self.stack.append(self.explanation_specifiers) + def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: - """Format the %-formatted string with current format context. - + """Format the %-formatted string with current format context. + The expl_expr should be an str ast.expr instance constructed from - 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 - return the ast.Name instance of the formatted string. - """ - current = self.stack.pop() - if self.stack: - self.explanation_specifiers = self.stack[-1] - keys = [ast.Str(key) for key in current.keys()] - format_dict = ast.Dict(keys, list(current.values())) - form = ast.BinOp(expl_expr, ast.Mod(), format_dict) - name = "@py_format" + str(next(self.variable_counter)) + return the ast.Name instance of the formatted string. + """ + current = self.stack.pop() + if self.stack: + self.explanation_specifiers = self.stack[-1] + keys = [ast.Str(key) for key in current.keys()] + format_dict = ast.Dict(keys, list(current.values())) + form = ast.BinOp(expl_expr, ast.Mod(), format_dict) + name = "@py_format" + str(next(self.variable_counter)) if self.enable_assertion_pass_hook: self.format_variables.append(name) self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) - return ast.Name(name, ast.Load()) - + return ast.Name(name, ast.Load()) + def generic_visit(self, node: ast.AST) -> Tuple[ast.Name, str]: - """Handle expressions we don't have custom code for.""" - assert isinstance(node, ast.expr) - res = self.assign(node) - return res, self.explanation_param(self.display(res)) - + """Handle expressions we don't have custom code for.""" + assert isinstance(node, ast.expr) + res = self.assign(node) + return res, self.explanation_param(self.display(res)) + def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: - """Return the AST statements to replace the ast.Assert instance. - - This rewrites the test of an assertion to provide - intermediate values and replace it with an if statement which - raises an assertion error with a detailed explanation in case - the expression is false. - """ - if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: + """Return the AST statements to replace the ast.Assert instance. + + This rewrites the test of an assertion to provide + intermediate values and replace it with an if statement which + raises an assertion error with a detailed explanation in case + the expression is false. + """ + if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: from _pytest.warning_types import PytestAssertRewriteWarning - import warnings - + import warnings + # TODO: This assert should not be needed. assert self.module_path is not None - warnings.warn_explicit( + warnings.warn_explicit( PytestAssertRewriteWarning( "assertion is always true, perhaps remove parentheses?" ), - category=None, + category=None, filename=os.fspath(self.module_path), - lineno=assert_.lineno, - ) - + lineno=assert_.lineno, + ) + self.statements: List[ast.stmt] = [] self.variables: List[str] = [] - self.variable_counter = itertools.count() + self.variable_counter = itertools.count() if self.enable_assertion_pass_hook: self.format_variables: List[str] = [] self.stack: List[Dict[str, ast.expr]] = [] self.expl_stmts: List[ast.stmt] = [] - self.push_format_context() - # Rewrite assert into a bunch of statements. - top_condition, explanation = self.visit(assert_.test) + self.push_format_context() + # Rewrite assert into a bunch of statements. + top_condition, explanation = self.visit(assert_.test) negation = ast.UnaryOp(ast.Not(), top_condition) @@ -937,156 +937,156 @@ class AssertionRewriter(ast.NodeVisitor): fmt = self.helper("_format_explanation", msg) err_name = ast.Name("AssertionError", ast.Load()) exc = ast.Call(err_name, [fmt], []) - raise_ = ast.Raise(exc, None) + raise_ = ast.Raise(exc, None) body.append(raise_) - # Clear temporary variables by setting them to None. - if self.variables: - variables = [ast.Name(name, ast.Store()) for name in self.variables] + # Clear temporary variables by setting them to None. + if self.variables: + variables = [ast.Name(name, ast.Store()) for name in self.variables] clear = ast.Assign(variables, ast.NameConstant(None)) - self.statements.append(clear) - # Fix line numbers. - for stmt in self.statements: - set_location(stmt, assert_.lineno, assert_.col_offset) - return self.statements - + self.statements.append(clear) + # Fix line numbers. + for stmt in self.statements: + set_location(stmt, assert_.lineno, assert_.col_offset) + return self.statements + def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: - # Display the repr of the name if it's a local variable or - # _should_repr_global_name() thinks it's acceptable. + # Display the repr of the name if it's a local variable or + # _should_repr_global_name() thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) - inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) - test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) - return name, self.explanation_param(expr) - + test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) + expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) + return name, self.explanation_param(expr) + def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: - res_var = self.variable() - expl_list = self.assign(ast.List([], ast.Load())) - app = ast.Attribute(expl_list, "append", ast.Load()) - is_or = int(isinstance(boolop.op, ast.Or)) - body = save = self.statements + res_var = self.variable() + expl_list = self.assign(ast.List([], ast.Load())) + app = ast.Attribute(expl_list, "append", ast.Load()) + is_or = int(isinstance(boolop.op, ast.Or)) + body = save = self.statements fail_save = self.expl_stmts - levels = len(boolop.values) - 1 - self.push_format_context() + levels = len(boolop.values) - 1 + self.push_format_context() # Process each operand, short-circuiting if needed. - for i, v in enumerate(boolop.values): - if i: + for i, v in enumerate(boolop.values): + if i: fail_inner: List[ast.stmt] = [] - # cond is set in a prior loop iteration below + # cond is set in a prior loop iteration below self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts = fail_inner - self.push_format_context() - res, expl = self.visit(v) - body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) - expl_format = self.pop_format_context(ast.Str(expl)) + self.push_format_context() + res, expl = self.visit(v) + body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) + expl_format = self.pop_format_context(ast.Str(expl)) call = ast.Call(app, [expl_format], []) self.expl_stmts.append(ast.Expr(call)) - if i < levels: + if i < levels: cond: ast.expr = res - if is_or: - cond = ast.UnaryOp(ast.Not(), cond) + if is_or: + cond = ast.UnaryOp(ast.Not(), cond) inner: List[ast.stmt] = [] - self.statements.append(ast.If(cond, inner, [])) - self.statements = body = inner - self.statements = save + self.statements.append(ast.If(cond, inner, [])) + self.statements = body = inner + self.statements = save self.expl_stmts = fail_save expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) - expl = self.pop_format_context(expl_template) - return ast.Name(res_var, ast.Load()), self.explanation_param(expl) - + expl = self.pop_format_context(expl_template) + return ast.Name(res_var, ast.Load()), self.explanation_param(expl) + def visit_UnaryOp(self, unary: ast.UnaryOp) -> Tuple[ast.Name, str]: pattern = UNARY_MAP[unary.op.__class__] - operand_res, operand_expl = self.visit(unary.operand) - res = self.assign(ast.UnaryOp(unary.op, operand_res)) - return res, pattern % (operand_expl,) - + operand_res, operand_expl = self.visit(unary.operand) + res = self.assign(ast.UnaryOp(unary.op, operand_res)) + return res, pattern % (operand_expl,) + def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]: symbol = BINOP_MAP[binop.op.__class__] - left_expr, left_expl = self.visit(binop.left) - right_expr, right_expl = self.visit(binop.right) + left_expr, left_expl = self.visit(binop.left) + right_expr, right_expl = self.visit(binop.right) explanation = f"({left_expl} {symbol} {right_expl})" - res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) - return res, explanation - + res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) + return res, explanation + def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: - new_func, func_expl = self.visit(call.func) - arg_expls = [] - new_args = [] - new_kwargs = [] - for arg in call.args: - res, expl = self.visit(arg) - arg_expls.append(expl) - new_args.append(res) - for keyword in call.keywords: - res, expl = self.visit(keyword.value) - new_kwargs.append(ast.keyword(keyword.arg, res)) - if keyword.arg: - arg_expls.append(keyword.arg + "=" + expl) - else: # **args have `arg` keywords with an .arg of None - arg_expls.append("**" + expl) - + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + for arg in call.args: + res, expl = self.visit(arg) + arg_expls.append(expl) + new_args.append(res) + for keyword in call.keywords: + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + if keyword.arg: + arg_expls.append(keyword.arg + "=" + expl) + else: # **args have `arg` keywords with an .arg of None + arg_expls.append("**" + expl) + expl = "{}({})".format(func_expl, ", ".join(arg_expls)) - new_call = ast.Call(new_func, new_args, new_kwargs) - res = self.assign(new_call) - res_expl = self.explanation_param(self.display(res)) + new_call = ast.Call(new_func, new_args, new_kwargs) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}" - return res, outer_expl - + return res, outer_expl + def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]: # A Starred node can appear in a function call. - res, expl = self.visit(starred.value) - new_starred = ast.Starred(res, starred.ctx) - return new_starred, "*" + expl - + res, expl = self.visit(starred.value) + new_starred = ast.Starred(res, starred.ctx) + return new_starred, "*" + expl + def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]: - if not isinstance(attr.ctx, ast.Load): - return self.generic_visit(attr) - value, value_expl = self.visit(attr.value) - res = self.assign(ast.Attribute(value, attr.attr, ast.Load())) - res_expl = self.explanation_param(self.display(res)) - pat = "%s\n{%s = %s.%s\n}" - expl = pat % (res_expl, res_expl, value_expl, attr.attr) - return res, expl - + if not isinstance(attr.ctx, ast.Load): + return self.generic_visit(attr) + value, value_expl = self.visit(attr.value) + res = self.assign(ast.Attribute(value, attr.attr, ast.Load())) + res_expl = self.explanation_param(self.display(res)) + pat = "%s\n{%s = %s.%s\n}" + expl = pat % (res_expl, res_expl, value_expl, attr.attr) + return res, expl + def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: - self.push_format_context() - left_res, left_expl = self.visit(comp.left) - if isinstance(comp.left, (ast.Compare, ast.BoolOp)): + self.push_format_context() + left_res, left_expl = self.visit(comp.left) + if isinstance(comp.left, (ast.Compare, ast.BoolOp)): left_expl = f"({left_expl})" - res_variables = [self.variable() for i in range(len(comp.ops))] - load_names = [ast.Name(v, ast.Load()) for v in res_variables] - store_names = [ast.Name(v, ast.Store()) for v in res_variables] - it = zip(range(len(comp.ops)), comp.ops, comp.comparators) - expls = [] - syms = [] - results = [left_res] - for i, op, next_operand in it: - next_res, next_expl = self.visit(next_operand) - if isinstance(next_operand, (ast.Compare, ast.BoolOp)): + res_variables = [self.variable() for i in range(len(comp.ops))] + load_names = [ast.Name(v, ast.Load()) for v in res_variables] + store_names = [ast.Name(v, ast.Store()) for v in res_variables] + it = zip(range(len(comp.ops)), comp.ops, comp.comparators) + expls = [] + syms = [] + results = [left_res] + for i, op, next_operand in it: + next_res, next_expl = self.visit(next_operand) + if isinstance(next_operand, (ast.Compare, ast.BoolOp)): next_expl = f"({next_expl})" - results.append(next_res) + results.append(next_res) sym = BINOP_MAP[op.__class__] - syms.append(ast.Str(sym)) + syms.append(ast.Str(sym)) expl = f"{left_expl} {sym} {next_expl}" - expls.append(ast.Str(expl)) - res_expr = ast.Compare(left_res, [op], [next_res]) - self.statements.append(ast.Assign([store_names[i]], res_expr)) - left_res, left_expl = next_res, next_expl - # Use pytest.assertion.util._reprcompare if that's available. - expl_call = self.helper( + expls.append(ast.Str(expl)) + res_expr = ast.Compare(left_res, [op], [next_res]) + self.statements.append(ast.Assign([store_names[i]], res_expr)) + left_res, left_expl = next_res, next_expl + # Use pytest.assertion.util._reprcompare if that's available. + expl_call = self.helper( "_call_reprcompare", - ast.Tuple(syms, ast.Load()), - ast.Tuple(load_names, ast.Load()), - ast.Tuple(expls, ast.Load()), - ast.Tuple(results, ast.Load()), - ) - if len(comp.ops) > 1: + ast.Tuple(syms, ast.Load()), + ast.Tuple(load_names, ast.Load()), + ast.Tuple(expls, ast.Load()), + ast.Tuple(results, ast.Load()), + ) + if len(comp.ops) > 1: res: ast.expr = ast.BoolOp(ast.And(), load_names) - else: - res = load_names[0] - return res, self.explanation_param(self.pop_format_context(expl_call)) + else: + res = load_names[0] + return res, self.explanation_param(self.pop_format_context(expl_call)) def try_makedirs(cache_dir: Path) -> bool: diff --git a/contrib/python/pytest/py3/_pytest/assertion/truncate.py b/contrib/python/pytest/py3/_pytest/assertion/truncate.py index bdd2761bb1..5ba9ddca75 100644 --- a/contrib/python/pytest/py3/_pytest/assertion/truncate.py +++ b/contrib/python/pytest/py3/_pytest/assertion/truncate.py @@ -1,100 +1,100 @@ """Utilities for truncating assertion output. - -Current default behaviour is to truncate assertion explanations at -~8 terminal lines, unless running in "-vv" mode or running on CI. -""" -import os + +Current default behaviour is to truncate assertion explanations at +~8 terminal lines, unless running in "-vv" mode or running on CI. +""" +import os from typing import List from typing import Optional - + from _pytest.nodes import Item -DEFAULT_MAX_LINES = 8 -DEFAULT_MAX_CHARS = 8 * 80 -USAGE_MSG = "use '-vv' to show" - - +DEFAULT_MAX_LINES = 8 +DEFAULT_MAX_CHARS = 8 * 80 +USAGE_MSG = "use '-vv' to show" + + def truncate_if_required( explanation: List[str], item: Item, max_length: Optional[int] = None ) -> List[str]: """Truncate this assertion explanation if the given test item is eligible.""" - if _should_truncate_item(item): - return _truncate_explanation(explanation) - return explanation - - + if _should_truncate_item(item): + return _truncate_explanation(explanation) + return explanation + + def _should_truncate_item(item: Item) -> bool: """Whether or not this test item is eligible for truncation.""" - verbose = item.config.option.verbose - return verbose < 2 and not _running_on_ci() - - + verbose = item.config.option.verbose + return verbose < 2 and not _running_on_ci() + + def _running_on_ci() -> bool: - """Check if we're currently running on a CI system.""" - env_vars = ["CI", "BUILD_NUMBER"] - return any(var in os.environ for var in env_vars) - - + """Check if we're currently running on a CI system.""" + env_vars = ["CI", "BUILD_NUMBER"] + return any(var in os.environ for var in env_vars) + + def _truncate_explanation( input_lines: List[str], max_lines: Optional[int] = None, max_chars: Optional[int] = None, ) -> List[str]: """Truncate given list of strings that makes up the assertion explanation. - - Truncates to either 8 lines, or 640 characters - whichever the input reaches - first. The remaining lines will be replaced by a usage message. - """ - - if max_lines is None: - max_lines = DEFAULT_MAX_LINES - if max_chars is None: - max_chars = DEFAULT_MAX_CHARS - - # Check if truncation required - input_char_count = len("".join(input_lines)) - if len(input_lines) <= max_lines and input_char_count <= max_chars: - return input_lines - - # Truncate first to max_lines, and then truncate to max_chars if max_chars - # is exceeded. - truncated_explanation = input_lines[:max_lines] - truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars) - - # Add ellipsis to final line - truncated_explanation[-1] = truncated_explanation[-1] + "..." - - # Append useful message to explanation - truncated_line_count = len(input_lines) - len(truncated_explanation) - truncated_line_count += 1 # Account for the part-truncated final line - msg = "...Full output truncated" - if truncated_line_count == 1: + + Truncates to either 8 lines, or 640 characters - whichever the input reaches + first. The remaining lines will be replaced by a usage message. + """ + + if max_lines is None: + max_lines = DEFAULT_MAX_LINES + if max_chars is None: + max_chars = DEFAULT_MAX_CHARS + + # Check if truncation required + input_char_count = len("".join(input_lines)) + if len(input_lines) <= max_lines and input_char_count <= max_chars: + return input_lines + + # Truncate first to max_lines, and then truncate to max_chars if max_chars + # is exceeded. + truncated_explanation = input_lines[:max_lines] + truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars) + + # Add ellipsis to final line + truncated_explanation[-1] = truncated_explanation[-1] + "..." + + # Append useful message to explanation + truncated_line_count = len(input_lines) - len(truncated_explanation) + truncated_line_count += 1 # Account for the part-truncated final line + msg = "...Full output truncated" + if truncated_line_count == 1: msg += f" ({truncated_line_count} line hidden)" - else: + else: msg += f" ({truncated_line_count} lines hidden)" msg += f", {USAGE_MSG}" truncated_explanation.extend(["", str(msg)]) - return truncated_explanation - - + return truncated_explanation + + def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]: - # Check if truncation required - if len("".join(input_lines)) <= max_chars: - return input_lines - - # Find point at which input length exceeds total allowed length - iterated_char_count = 0 - for iterated_index, input_line in enumerate(input_lines): - if iterated_char_count + len(input_line) > max_chars: - break - iterated_char_count += len(input_line) - - # Create truncated explanation with modified final line - truncated_result = input_lines[:iterated_index] - final_line = input_lines[iterated_index] - if final_line: - final_line_truncate_point = max_chars - iterated_char_count - final_line = final_line[:final_line_truncate_point] - truncated_result.append(final_line) - return truncated_result + # Check if truncation required + if len("".join(input_lines)) <= max_chars: + return input_lines + + # Find point at which input length exceeds total allowed length + iterated_char_count = 0 + for iterated_index, input_line in enumerate(input_lines): + if iterated_char_count + len(input_line) > max_chars: + break + iterated_char_count += len(input_line) + + # Create truncated explanation with modified final line + truncated_result = input_lines[:iterated_index] + final_line = input_lines[iterated_index] + if final_line: + final_line_truncate_point = max_chars - iterated_char_count + final_line = final_line[:final_line_truncate_point] + truncated_result.append(final_line) + return truncated_result diff --git a/contrib/python/pytest/py3/_pytest/assertion/util.py b/contrib/python/pytest/py3/_pytest/assertion/util.py index 0774d36fbc..da1ffd15e3 100644 --- a/contrib/python/pytest/py3/_pytest/assertion/util.py +++ b/contrib/python/pytest/py3/_pytest/assertion/util.py @@ -1,6 +1,6 @@ """Utilities for assertion debugging.""" import collections.abc -import pprint +import pprint from typing import AbstractSet from typing import Any from typing import Callable @@ -9,106 +9,106 @@ from typing import List from typing import Mapping from typing import Optional from typing import Sequence - -import _pytest._code + +import _pytest._code from _pytest import outcomes from _pytest._io.saferepr import _pformat_dispatch from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr - -# The _reprcompare attribute on the util module is used by the new assertion -# interpretation code and assertion rewriter to detect this plugin was -# loaded and in turn call the hooks defined here as part of the -# DebugInterpreter. + +# The _reprcompare attribute on the util module is used by the new assertion +# interpretation code and assertion rewriter to detect this plugin was +# loaded and in turn call the hooks defined here as part of the +# DebugInterpreter. _reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None - + # Works similarly as _reprcompare attribute. Is populated with the hook call # when pytest_runtest_setup is called. _assertion_pass: Optional[Callable[[int, str, str], None]] = None - - + + def format_explanation(explanation: str) -> str: r"""Format an explanation. - - Normally all embedded newlines are escaped, however there are - three exceptions: \n{, \n} and \n~. The first two are intended - cover nested explanations, see function and attribute explanations - for examples (.visit_Call(), visit_Attribute()). The last one is - for when one explanation needs to span multiple lines, e.g. when - displaying diffs. - """ - lines = _split_explanation(explanation) - result = _format_lines(lines) + + Normally all embedded newlines are escaped, however there are + three exceptions: \n{, \n} and \n~. The first two are intended + cover nested explanations, see function and attribute explanations + for examples (.visit_Call(), visit_Attribute()). The last one is + for when one explanation needs to span multiple lines, e.g. when + displaying diffs. + """ + lines = _split_explanation(explanation) + result = _format_lines(lines) return "\n".join(result) - - + + def _split_explanation(explanation: str) -> List[str]: r"""Return a list of individual lines in the explanation. - - 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 - literal '\n' characters. - """ + + 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 + literal '\n' characters. + """ raw_lines = (explanation or "").split("\n") - lines = [raw_lines[0]] - for values in raw_lines[1:]: - if values and values[0] in ["{", "}", "~", ">"]: - lines.append(values) - else: - lines[-1] += "\\n" + values - return lines - - + lines = [raw_lines[0]] + for values in raw_lines[1:]: + if values and values[0] in ["{", "}", "~", ">"]: + lines.append(values) + else: + lines[-1] += "\\n" + values + return lines + + def _format_lines(lines: Sequence[str]) -> List[str]: """Format the individual lines. - + This will replace the '{', '}' and '~' characters of our mini formatting language with the proper 'where ...', 'and ...' and ' + ...' text, taking care of indentation along the way. - - Return a list of formatted lines. - """ + + Return a list of formatted lines. + """ result = list(lines[:1]) - stack = [0] - stackcnt = [0] - for line in lines[1:]: - if line.startswith("{"): - if stackcnt[-1]: + stack = [0] + stackcnt = [0] + for line in lines[1:]: + if line.startswith("{"): + if stackcnt[-1]: s = "and " - else: + else: s = "where " - stack.append(len(result)) - stackcnt[-1] += 1 - stackcnt.append(0) + stack.append(len(result)) + stackcnt[-1] += 1 + stackcnt.append(0) result.append(" +" + " " * (len(stack) - 1) + s + line[1:]) - elif line.startswith("}"): - stack.pop() - stackcnt.pop() - result[stack[-1]] += line[1:] - else: - assert line[0] in ["~", ">"] - stack[-1] += 1 - indent = len(stack) if line.startswith("~") else len(stack) - 1 + elif line.startswith("}"): + stack.pop() + stackcnt.pop() + result[stack[-1]] += line[1:] + else: + assert line[0] in ["~", ">"] + stack[-1] += 1 + indent = len(stack) if line.startswith("~") else len(stack) - 1 result.append(" " * indent + line[1:]) - assert len(stack) == 1 - return result - - + assert len(stack) == 1 + return result + + def issequence(x: Any) -> bool: return isinstance(x, collections.abc.Sequence) and not isinstance(x, str) - - + + def istext(x: Any) -> bool: return isinstance(x, str) - - + + def isdict(x: Any) -> bool: return isinstance(x, dict) - - + + def isset(x: Any) -> bool: return isinstance(x, (set, frozenset)) - + def isnamedtuple(obj: Any) -> bool: return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None @@ -147,29 +147,29 @@ def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[ summary = f"{left_repr} {op} {right_repr}" - explanation = None - try: - if op == "==": + explanation = None + try: + if op == "==": explanation = _compare_eq_any(left, right, verbose) - elif op == "not in": - if istext(left) and istext(right): - explanation = _notin_text(left, right, verbose) + elif op == "not in": + if istext(left) and istext(right): + explanation = _notin_text(left, right, verbose) except outcomes.Exit: raise - except Exception: - explanation = [ + except Exception: + explanation = [ "(pytest_assertion plugin: representation of details failed: {}.".format( _pytest._code.ExceptionInfo.from_current()._getreprcrash() ), " Probably an object has a faulty __repr__.)", - ] - - if not explanation: - return None - - return [summary] + explanation - - + ] + + if not explanation: + return None + + return [summary] + explanation + + def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: explanation = [] if istext(left) and istext(right): @@ -199,52 +199,52 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: """Return the explanation for the diff between text. - - Unless --verbose is used this will skip leading and trailing - characters which are identical to keep the diff minimal. - """ - from difflib import ndiff - + + Unless --verbose is used this will skip leading and trailing + characters which are identical to keep the diff minimal. + """ + from difflib import ndiff + explanation: List[str] = [] - + if verbose < 1: - i = 0 # just in case left or right has zero length - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - break - if i > 42: - i -= 10 # Provide some context - explanation = [ + i = 0 # just in case left or right has zero length + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + break + if i > 42: + i -= 10 # Provide some context + explanation = [ "Skipping %s identical leading characters in diff, use -v to show" % i - ] - left = left[i:] - right = right[i:] - if len(left) == len(right): - for i in range(len(left)): - if left[-i] != right[-i]: - break - if i > 42: - i -= 10 # Provide some context - explanation += [ + ] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += [ "Skipping {} identical trailing " "characters in diff, use -v to show".format(i) - ] - left = left[:-i] - right = right[:-i] - keepends = True - if left.isspace() or right.isspace(): - left = repr(str(left)) - right = repr(str(right)) + ] + left = left[:-i] + right = right[:-i] + keepends = True + if left.isspace() or right.isspace(): + left = repr(str(left)) + right = repr(str(right)) explanation += ["Strings contain only whitespace, escaping them using repr()"] # "right" is the expected base against which we compare "left", # see https://github.com/pytest-dev/pytest/issues/3333 - explanation += [ - line.strip("\n") + explanation += [ + line.strip("\n") for line in ndiff(right.splitlines(keepends), left.splitlines(keepends)) - ] - return explanation - - + ] + return explanation + + def _compare_eq_verbose(left: Any, right: Any) -> List[str]: keepends = True left_lines = repr(left).splitlines(keepends) @@ -272,11 +272,11 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None: def _compare_eq_iterable( left: Iterable[Any], right: Iterable[Any], verbose: int = 0 ) -> List[str]: - if not verbose: + if not verbose: return ["Use -v to get the full diff"] - # dynamic import to speedup pytest - import difflib - + # dynamic import to speedup pytest + import difflib + left_formatting = pprint.pformat(left).splitlines() right_formatting = pprint.pformat(right).splitlines() @@ -294,12 +294,12 @@ def _compare_eq_iterable( explanation = ["Full diff:"] # "right" is the expected base against which we compare "left", # see https://github.com/pytest-dev/pytest/issues/3333 - explanation.extend( + explanation.extend( line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting) - ) - return explanation - - + ) + return explanation + + def _compare_eq_sequence( left: Sequence[Any], right: Sequence[Any], verbose: int = 0 ) -> List[str]: @@ -308,7 +308,7 @@ def _compare_eq_sequence( len_left = len(left) len_right = len(right) for i in range(min(len_left, len_right)): - if left[i] != right[i]: + if left[i] != right[i]: if comparing_bytes: # when comparing bytes, we want to see their ascii representation # instead of their numeric values (#5260) @@ -325,7 +325,7 @@ def _compare_eq_sequence( right_value = right[i] explanation += [f"At index {i} diff: {left_value!r} != {right_value!r}"] - break + break if comparing_bytes: # when comparing bytes, it doesn't help to show the "sides contain one or more @@ -350,26 +350,26 @@ def _compare_eq_sequence( "%s contains %d more items, first extra item: %s" % (dir_with_more, len_diff, extra) ] - return explanation - - + return explanation + + def _compare_eq_set( left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 ) -> List[str]: - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: explanation.append("Extra items in the left set:") - for item in diff_left: + for item in diff_left: explanation.append(saferepr(item)) - if diff_right: + if diff_right: explanation.append("Extra items in the right set:") - for item in diff_right: + for item in diff_right: explanation.append(saferepr(item)) - return explanation - - + return explanation + + def _compare_eq_dict( left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0 ) -> List[str]: @@ -377,16 +377,16 @@ def _compare_eq_dict( set_left = set(left) set_right = set(right) common = set_left.intersection(set_right) - same = {k: left[k] for k in common if left[k] == right[k]} - if same and verbose < 2: + same = {k: left[k] for k in common if left[k] == right[k]} + if same and verbose < 2: explanation += ["Omitting %s identical items, use -vv to show" % len(same)] - elif same: + elif same: explanation += ["Common items:"] - explanation += pprint.pformat(same).splitlines() - diff = {k for k in common if left[k] != right[k]} - if diff: + explanation += pprint.pformat(same).splitlines() + diff = {k for k in common if left[k] != right[k]} + if diff: explanation += ["Differing items:"] - for k in diff: + for k in diff: explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] extra_left = set_left - set_right len_extra_left = len(extra_left) @@ -395,9 +395,9 @@ def _compare_eq_dict( "Left contains %d more item%s:" % (len_extra_left, "" if len_extra_left == 1 else "s") ) - explanation.extend( - pprint.pformat({k: left[k] for k in extra_left}).splitlines() - ) + explanation.extend( + pprint.pformat({k: left[k] for k in extra_left}).splitlines() + ) extra_right = set_right - set_left len_extra_right = len(extra_right) if len_extra_right: @@ -405,12 +405,12 @@ def _compare_eq_dict( "Right contains %d more item%s:" % (len_extra_right, "" if len_extra_right == 1 else "s") ) - explanation.extend( - pprint.pformat({k: right[k] for k in extra_right}).splitlines() - ) - return explanation - - + explanation.extend( + pprint.pformat({k: right[k] for k in extra_right}).splitlines() + ) + return explanation + + def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: if isdatacls(left): all_fields = left.__dataclass_fields__ @@ -459,19 +459,19 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: - index = text.find(term) - head = text[:index] - tail = text[index + len(term) :] - correct_text = head + tail + index = text.find(term) + head = text[:index] + tail = text[index + len(term) :] + correct_text = head + tail diff = _diff_text(text, correct_text, verbose) newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)] - for line in diff: + for line in diff: if line.startswith("Skipping"): - continue + continue if line.startswith("- "): - continue + continue if line.startswith("+ "): newdiff.append(" " + line[2:]) - else: - newdiff.append(line) - return newdiff + else: + newdiff.append(line) + return newdiff diff --git a/contrib/python/pytest/py3/_pytest/cacheprovider.py b/contrib/python/pytest/py3/_pytest/cacheprovider.py index d67e5933cf..03acd03109 100644 --- a/contrib/python/pytest/py3/_pytest/cacheprovider.py +++ b/contrib/python/pytest/py3/_pytest/cacheprovider.py @@ -1,8 +1,8 @@ """Implementation of the cache provider.""" # This plugin was not named "cache" to avoid conflicts with the external # pytest-cache version. -import json -import os +import json +import os from pathlib import Path from typing import Dict from typing import Generator @@ -11,11 +11,11 @@ from typing import List from typing import Optional from typing import Set from typing import Union - -import attr -import py - -from .pathlib import resolve_from_str + +import attr +import py + +from .pathlib import resolve_from_str from .pathlib import rm_rf from .reports import CollectReport from _pytest import nodes @@ -32,33 +32,33 @@ from _pytest.main import Session from _pytest.python import Module from _pytest.python import Package from _pytest.reports import TestReport - + README_CONTENT = """\ -# pytest cache directory # - -This directory contains data from the pytest's cache plugin, -which provides the `--lf` and `--ff` options, as well as the `cache` fixture. - -**Do not** commit this to version control. - +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + See [the docs](https://docs.pytest.org/en/stable/cache.html) for more information. -""" - +""" + CACHEDIR_TAG_CONTENT = b"""\ Signature: 8a477f597d28d172789f06886806bc55 # This file is a cache directory tag created by pytest. # For information about cache directory tags, see: # http://www.bford.info/cachedir/spec.html """ - + @final @attr.s(init=False) class Cache: _cachedir = attr.ib(type=Path, repr=False) _config = attr.ib(type=Config, repr=False) - + # sub-directory under cache-dir for directories created by "makedir" _CACHE_PREFIX_DIRS = "d" @@ -72,7 +72,7 @@ class Cache: self._cachedir = cachedir self._config = config - @classmethod + @classmethod def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache": """Create the Cache instance for a Config. @@ -83,7 +83,7 @@ class Cache: if config.getoption("cacheclear") and cachedir.is_dir(): cls.clear_cache(cachedir, _ispytest=True) return cls(cachedir, config, _ispytest=True) - + @classmethod def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None: """Clear the sub-directories used to hold cached directories and values. @@ -96,10 +96,10 @@ class Cache: if d.is_dir(): rm_rf(d) - @staticmethod + @staticmethod def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path: """Get the path to the cache directory for a Config. - + :meta private: """ check_ispytest(_ispytest) @@ -113,16 +113,16 @@ class Cache: check_ispytest(_ispytest) import warnings from _pytest.warning_types import PytestCacheWarning - + warnings.warn( PytestCacheWarning(fmt.format(**args) if args else fmt), self._config.hook, - stacklevel=3, - ) - + stacklevel=3, + ) + def makedir(self, name: str) -> py.path.local: """Return a directory path object with the given name. - + If the directory does not yet exist, it will be created. You can use it to manage files to e.g. store/retrieve database dumps across test sessions. @@ -131,79 +131,79 @@ class Cache: 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) if len(path.parts) > 1: - raise ValueError("name is not allowed to contain path separators") + raise ValueError("name is not allowed to contain path separators") res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) - res.mkdir(exist_ok=True, parents=True) - return py.path.local(res) - + res.mkdir(exist_ok=True, parents=True) + return py.path.local(res) + def _getvaluepath(self, key: str) -> Path: return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) - + def get(self, key: str, default): """Return the cached value for the given key. 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 name is the name of your plugin or your application. :param default: The value to return in case of a cache-miss or invalid cache value. - """ - path = self._getvaluepath(key) - try: - with path.open("r") as f: - return json.load(f) + """ + path = self._getvaluepath(key) + try: + with path.open("r") as f: + return json.load(f) except (ValueError, OSError): - return default - + return default + def set(self, key: str, value: object) -> None: """Save value for the given key. - + :param key: Must be a ``/`` separated value. Usually the first name is the name of your plugin or your application. :param value: Must be of any combination of basic python types, including nested types like lists of dictionaries. - """ - path = self._getvaluepath(key) - try: - if path.parent.is_dir(): - cache_dir_exists_already = True - else: - cache_dir_exists_already = self._cachedir.exists() + """ + path = self._getvaluepath(key) + try: + if path.parent.is_dir(): + cache_dir_exists_already = True + else: + cache_dir_exists_already = self._cachedir.exists() path.parent.mkdir(exist_ok=True, parents=True) except OSError: self.warn("could not create cache path {path}", path=path, _ispytest=True) - return + return if not cache_dir_exists_already: self._ensure_supporting_files() data = json.dumps(value, indent=2, sort_keys=True) - try: + try: f = path.open("w") except OSError: self.warn("cache could not write path {path}", path=path, _ispytest=True) - else: - with f: + else: + with f: f.write(data) - + def _ensure_supporting_files(self) -> None: - """Create supporting files in the cache dir that are not really part of the cache.""" + """Create supporting files in the cache dir that are not really part of the cache.""" readme_path = self._cachedir / "README.md" readme_path.write_text(README_CONTENT) - + gitignore_path = self._cachedir.joinpath(".gitignore") msg = "# Created by pytest automatically.\n*\n" gitignore_path.write_text(msg, encoding="UTF-8") - + cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG") cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT) - + class LFPluginCollWrapper: def __init__(self, lfplugin: "LFPlugin") -> None: @@ -276,17 +276,17 @@ class LFPluginCollSkipfiles: class LFPlugin: """Plugin which implements the --lf (run last-failing) option.""" - + def __init__(self, config: Config) -> None: - self.config = config - active_keys = "lf", "failedfirst" - self.active = any(config.getoption(key) for key in active_keys) + self.config = config + active_keys = "lf", "failedfirst" + self.active = any(config.getoption(key) for key in active_keys) assert config.cache self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {}) self._previously_failed_count: Optional[int] = None self._report_status: Optional[str] = None self._skipped_files = 0 # count skipped files during collection due to --lf - + if config.getoption("lf"): self._last_failed_paths = self.get_last_failed_paths() config.pluginmanager.register( @@ -300,25 +300,25 @@ class LFPlugin: return {x for x in result if x.exists()} def pytest_report_collectionfinish(self) -> Optional[str]: - if self.active and self.config.getoption("verbose") >= 0: + if self.active and self.config.getoption("verbose") >= 0: return "run-last-failure: %s" % self._report_status return None - + def pytest_runtest_logreport(self, report: TestReport) -> None: - if (report.when == "call" and report.passed) or report.skipped: - self.lastfailed.pop(report.nodeid, None) - elif report.failed: - self.lastfailed[report.nodeid] = True - + if (report.when == "call" and report.passed) or report.skipped: + self.lastfailed.pop(report.nodeid, None) + elif report.failed: + self.lastfailed[report.nodeid] = True + def pytest_collectreport(self, report: CollectReport) -> None: - passed = report.outcome in ("passed", "skipped") - if passed: - if report.nodeid in self.lastfailed: - self.lastfailed.pop(report.nodeid) - self.lastfailed.update((item.nodeid, True) for item in report.result) - else: - self.lastfailed[report.nodeid] = True - + passed = report.outcome in ("passed", "skipped") + if passed: + if report.nodeid in self.lastfailed: + self.lastfailed.pop(report.nodeid) + self.lastfailed.update((item.nodeid, True) for item in report.result) + else: + self.lastfailed[report.nodeid] = True + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, config: Config, items: List[nodes.Item] @@ -345,11 +345,11 @@ class LFPlugin: len(self.lastfailed), ) else: - if self.config.getoption("lf"): - items[:] = previously_failed - config.hook.pytest_deselected(items=previously_passed) + if self.config.getoption("lf"): + items[:] = previously_failed + config.hook.pytest_deselected(items=previously_passed) else: # --failedfirst - items[:] = previously_failed + previously_passed + items[:] = previously_failed + previously_passed noun = "failure" if self._previously_failed_count == 1 else "failures" suffix = " first" if self.config.getoption("failedfirst") else "" @@ -367,209 +367,209 @@ class LFPlugin: if self.config.getoption("last_failed_no_failures") == "none": self._report_status += "deselecting all items." config.hook.pytest_deselected(items=items[:]) - items[:] = [] + items[:] = [] else: self._report_status += "not deselecting items." - + def pytest_sessionfinish(self, session: Session) -> None: - config = self.config + config = self.config if config.getoption("cacheshow") or hasattr(config, "workerinput"): - return - + return + assert config.cache is not None - saved_lastfailed = config.cache.get("cache/lastfailed", {}) - if saved_lastfailed != self.lastfailed: - config.cache.set("cache/lastfailed", self.lastfailed) - - + saved_lastfailed = config.cache.get("cache/lastfailed", {}) + if saved_lastfailed != self.lastfailed: + config.cache.set("cache/lastfailed", self.lastfailed) + + class NFPlugin: """Plugin which implements the --nf (run new-first) option.""" - + def __init__(self, config: Config) -> None: - self.config = config - self.active = config.option.newfirst + self.config = config + self.active = config.option.newfirst assert config.cache is not None self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) - + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, items: List[nodes.Item] ) -> Generator[None, None, None]: yield - if self.active: + if self.active: new_items: Dict[str, nodes.Item] = {} other_items: Dict[str, nodes.Item] = {} - for item in items: - if item.nodeid not in self.cached_nodeids: - new_items[item.nodeid] = item - else: - other_items[item.nodeid] = item - - items[:] = self._get_increasing_order( + for item in items: + if item.nodeid not in self.cached_nodeids: + new_items[item.nodeid] = item + else: + other_items[item.nodeid] = item + + items[:] = self._get_increasing_order( new_items.values() ) + self._get_increasing_order(other_items.values()) self.cached_nodeids.update(new_items) else: self.cached_nodeids.update(item.nodeid for item in items) - + def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) # type: ignore[no-any-return] - + def pytest_sessionfinish(self) -> None: - config = self.config + config = self.config if config.getoption("cacheshow") or hasattr(config, "workerinput"): - return - + return + if config.getoption("collectonly"): return - + assert config.cache is not None config.cache.set("cache/nodeids", sorted(self.cached_nodeids)) - + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group.addoption( - "--lf", - "--last-failed", - action="store_true", - dest="lf", - help="rerun only the tests that failed " - "at the last run (or all if none failed)", - ) - group.addoption( - "--ff", - "--failed-first", - action="store_true", - dest="failedfirst", + group = parser.getgroup("general") + group.addoption( + "--lf", + "--last-failed", + action="store_true", + dest="lf", + help="rerun only the tests that failed " + "at the last run (or all if none failed)", + ) + group.addoption( + "--ff", + "--failed-first", + action="store_true", + dest="failedfirst", help="run all tests, but run the last failures first.\n" - "This may re-order tests and thus lead to " + "This may re-order tests and thus lead to " "repeated fixture setup/teardown.", - ) - group.addoption( - "--nf", - "--new-first", - action="store_true", - dest="newfirst", - help="run tests from new files first, then the rest of the tests " - "sorted by file mtime", - ) - group.addoption( - "--cache-show", + ) + group.addoption( + "--nf", + "--new-first", + action="store_true", + dest="newfirst", + help="run tests from new files first, then the rest of the tests " + "sorted by file mtime", + ) + group.addoption( + "--cache-show", action="append", nargs="?", - dest="cacheshow", + dest="cacheshow", help=( "show cache contents, don't perform collection or tests. " "Optional argument: glob (default: '*')." ), - ) - group.addoption( - "--cache-clear", - action="store_true", - dest="cacheclear", - help="remove all cache contents at start of test run.", - ) - cache_dir_default = ".pytest_cache" - if "TOX_ENV_DIR" in os.environ: - cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default) - parser.addini("cache_dir", default=cache_dir_default, help="cache directory path.") - group.addoption( - "--lfnf", - "--last-failed-no-failures", - action="store", - dest="last_failed_no_failures", - choices=("all", "none"), - default="all", + ) + group.addoption( + "--cache-clear", + action="store_true", + dest="cacheclear", + help="remove all cache contents at start of test run.", + ) + cache_dir_default = ".pytest_cache" + if "TOX_ENV_DIR" in os.environ: + cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default) + parser.addini("cache_dir", default=cache_dir_default, help="cache directory path.") + group.addoption( + "--lfnf", + "--last-failed-no-failures", + action="store", + dest="last_failed_no_failures", + choices=("all", "none"), + default="all", help="which tests to run with no previously (known) failures.", - ) - - + ) + + def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: - if config.option.cacheshow: - from _pytest.main import wrap_session - - return wrap_session(config, cacheshow) + if config.option.cacheshow: + from _pytest.main import wrap_session + + return wrap_session(config, cacheshow) return None - - + + @hookimpl(tryfirst=True) def pytest_configure(config: Config) -> None: config.cache = Cache.for_config(config, _ispytest=True) - config.pluginmanager.register(LFPlugin(config), "lfplugin") - config.pluginmanager.register(NFPlugin(config), "nfplugin") - - + config.pluginmanager.register(LFPlugin(config), "lfplugin") + config.pluginmanager.register(NFPlugin(config), "nfplugin") + + @fixture def cache(request: FixtureRequest) -> Cache: """Return a cache object that can persist state between testing sessions. - - cache.get(key, default) - cache.set(key, value) - + + cache.get(key, default) + cache.set(key, value) + 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. - - Values can be any object handled by the json stdlib module. - """ + name of your plugin or application to avoid clashes with other cache users. + + Values can be any object handled by the json stdlib module. + """ assert request.config.cache is not None - return request.config.cache - - + return request.config.cache + + def pytest_report_header(config: Config) -> Optional[str]: - """Display cachedir with --cache-show and if non-default.""" + """Display cachedir with --cache-show and if non-default.""" if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache": assert config.cache is not None - cachedir = config.cache._cachedir - # TODO: evaluate generating upward relative paths - # starting with .., ../.. if sensible - - try: + cachedir = config.cache._cachedir + # TODO: evaluate generating upward relative paths + # starting with .., ../.. if sensible + + try: displaypath = cachedir.relative_to(config.rootpath) - except ValueError: - displaypath = cachedir + except ValueError: + displaypath = cachedir return f"cachedir: {displaypath}" return None - - + + def cacheshow(config: Config, session: Session) -> int: - from pprint import pformat - + from pprint import pformat + assert config.cache is not None tw = TerminalWriter() - tw.line("cachedir: " + str(config.cache._cachedir)) - if not config.cache._cachedir.is_dir(): - tw.line("cache is empty") - return 0 + tw.line("cachedir: " + str(config.cache._cachedir)) + if not config.cache._cachedir.is_dir(): + tw.line("cache is empty") + return 0 glob = config.option.cacheshow[0] if glob is None: glob = "*" - dummy = object() - basedir = config.cache._cachedir + dummy = object() + basedir = config.cache._cachedir vdir = basedir / Cache._CACHE_PREFIX_VALUES tw.sep("-", "cache values for %r" % glob) for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()): key = str(valpath.relative_to(vdir)) - val = config.cache.get(key, dummy) - if val is dummy: - tw.line("%s contains unreadable content, will be ignored" % key) - else: - tw.line("%s contains:" % key) - for line in pformat(val).splitlines(): - tw.line(" " + line) - + val = config.cache.get(key, dummy) + if val is dummy: + tw.line("%s contains unreadable content, will be ignored" % key) + else: + tw.line("%s contains:" % key) + for line in pformat(val).splitlines(): + tw.line(" " + line) + ddir = basedir / Cache._CACHE_PREFIX_DIRS - if ddir.is_dir(): + if ddir.is_dir(): contents = sorted(ddir.rglob(glob)) tw.sep("-", "cache directories for %r" % glob) - for p in contents: - # if p.check(dir=1): - # print("%s/" % p.relto(basedir)) - if p.is_file(): + for p in contents: + # if p.check(dir=1): + # print("%s/" % p.relto(basedir)) + if p.is_file(): key = str(p.relative_to(basedir)) tw.line(f"{key} is a file of length {p.stat().st_size:d}") - return 0 + return 0 diff --git a/contrib/python/pytest/py3/_pytest/capture.py b/contrib/python/pytest/py3/_pytest/capture.py index a94bebe8b1..086302658c 100644 --- a/contrib/python/pytest/py3/_pytest/capture.py +++ b/contrib/python/pytest/py3/_pytest/capture.py @@ -1,11 +1,11 @@ """Per-test stdout/stderr capturing mechanism.""" -import contextlib +import contextlib import functools -import io -import os -import sys -from io import UnsupportedOperation -from tempfile import TemporaryFile +import io +import os +import sys +from io import UnsupportedOperation +from tempfile import TemporaryFile from typing import Any from typing import AnyStr from typing import Generator @@ -16,7 +16,7 @@ from typing import TextIO from typing import Tuple from typing import TYPE_CHECKING from typing import Union - + from _pytest.compat import final from _pytest.config import Config from _pytest.config import hookimpl @@ -27,32 +27,32 @@ from _pytest.fixtures import SubRequest from _pytest.nodes import Collector from _pytest.nodes import File from _pytest.nodes import Item - + if TYPE_CHECKING: from typing_extensions import Literal _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] - + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group._addoption( - "--capture", - action="store", + group = parser.getgroup("general") + group._addoption( + "--capture", + action="store", default="fd", - metavar="method", + metavar="method", choices=["fd", "sys", "no", "tee-sys"], help="per-test capturing method: one of fd|sys|no|tee-sys.", - ) - group._addoption( - "-s", - action="store_const", - const="no", - dest="capture", - help="shortcut for --capture=no.", - ) - - + ) + group._addoption( + "-s", + action="store_const", + const="no", + dest="capture", + help="shortcut for --capture=no.", + ) + + def _colorama_workaround() -> None: """Ensure colorama is imported so that it attaches to the correct stdio handles on Windows. @@ -150,46 +150,46 @@ def _py36_windowsconsoleio_workaround(stream: TextIO) -> None: @hookimpl(hookwrapper=True) def pytest_load_initial_conftests(early_config: Config): - ns = early_config.known_args_namespace - if ns.capture == "fd": - _py36_windowsconsoleio_workaround(sys.stdout) - _colorama_workaround() - _readline_workaround() - pluginmanager = early_config.pluginmanager - capman = CaptureManager(ns.capture) - pluginmanager.register(capman, "capturemanager") - + ns = early_config.known_args_namespace + if ns.capture == "fd": + _py36_windowsconsoleio_workaround(sys.stdout) + _colorama_workaround() + _readline_workaround() + pluginmanager = early_config.pluginmanager + capman = CaptureManager(ns.capture) + pluginmanager.register(capman, "capturemanager") + # 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 (issue #93). - capman.start_global_capturing() - outcome = yield - capman.suspend_global_capture() - if outcome.excinfo is not None: - out, err = capman.read_global_capture() - sys.stdout.write(out) - sys.stderr.write(err) - - + capman.start_global_capturing() + outcome = yield + capman.suspend_global_capture() + if outcome.excinfo is not None: + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stderr.write(err) + + # IO Helpers. class EncodedFile(io.TextIOWrapper): __slots__ = () - + @property def name(self) -> str: # Ensure that file.name is a string. Workaround for a Python bug # fixed in >=3.7.4: https://bugs.python.org/issue36015 return repr(self.buffer) - + @property def mode(self) -> str: # TextIOWrapper doesn't expose a mode, but at least some of our # tests check it. return self.buffer.mode.replace("b", "") - + class CaptureIO(io.TextIOWrapper): def __init__(self) -> None: @@ -224,36 +224,36 @@ class DontReadFromInput: def __iter__(self): return self - + def fileno(self) -> int: raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") - + def isatty(self) -> bool: return False - + def close(self) -> None: pass - + @property def buffer(self): return self - - + + # Capture classes. patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} - - + + class NoCapture: EMPTY_BUFFER = None __init__ = start = done = suspend = resume = lambda *args: None class SysCaptureBinary: - + EMPTY_BUFFER = b"" - + def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None: name = patchsysdict[fd] self._old = getattr(sys, name) @@ -265,7 +265,7 @@ class SysCaptureBinary: tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) self.tmpfile = tmpfile self._state = "initialized" - + def repr(self, class_name: str) -> str: return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( class_name, @@ -274,7 +274,7 @@ class SysCaptureBinary: self._state, self.tmpfile, ) - + def __repr__(self) -> str: return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( self.__class__.__name__, @@ -283,19 +283,19 @@ class SysCaptureBinary: self._state, self.tmpfile, ) - + def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: assert ( self._state in states ), "cannot {} in state {!r}: expected one of {}".format( op, self._state, ", ".join(states) ) - + def start(self) -> None: self._assert_state("start", ("initialized",)) setattr(sys, self.name, self.tmpfile) self._state = "started" - + def snap(self): self._assert_state("snap", ("started", "suspended")) self.tmpfile.seek(0) @@ -303,7 +303,7 @@ class SysCaptureBinary: self.tmpfile.seek(0) self.tmpfile.truncate() return res - + def done(self) -> None: self._assert_state("done", ("initialized", "started", "suspended", "done")) if self._state == "done": @@ -312,49 +312,49 @@ class SysCaptureBinary: del self._old self.tmpfile.close() self._state = "done" - + def suspend(self) -> None: self._assert_state("suspend", ("started", "suspended")) setattr(sys, self.name, self._old) self._state = "suspended" - + def resume(self) -> None: self._assert_state("resume", ("started", "suspended")) if self._state == "started": return setattr(sys, self.name, self.tmpfile) self._state = "started" - + def writeorg(self, data) -> None: self._assert_state("writeorg", ("started", "suspended")) self._old.flush() self._old.buffer.write(data) self._old.buffer.flush() - - + + class SysCapture(SysCaptureBinary): EMPTY_BUFFER = "" # type: ignore[assignment] - + def snap(self): res = self.tmpfile.getvalue() self.tmpfile.seek(0) self.tmpfile.truncate() return res - + def writeorg(self, data): self._assert_state("writeorg", ("started", "suspended")) self._old.write(data) self._old.flush() - + class FDCaptureBinary: """Capture IO to/from a given OS-level file descriptor. snap() produces `bytes`. - """ - + """ + EMPTY_BUFFER = b"" - + def __init__(self, targetfd: int) -> None: self.targetfd = targetfd @@ -377,7 +377,7 @@ class FDCaptureBinary: else: self.targetfd_invalid = None self.targetfd_save = os.dup(targetfd) - + if targetfd == 0: self.tmpfile = open(os.devnull) self.syscapture = SysCapture(targetfd) @@ -393,7 +393,7 @@ class FDCaptureBinary: self.syscapture = SysCapture(targetfd, self.tmpfile) else: self.syscapture = NoCapture() - + self._state = "initialized" def __repr__(self) -> str: @@ -403,15 +403,15 @@ class FDCaptureBinary: self.targetfd_save, self._state, self.tmpfile, - ) - + ) + def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: assert ( self._state in states ), "cannot {} in state {!r}: expected one of {}".format( op, self._state, ", ".join(states) ) - + def start(self) -> None: """Start capturing on targetfd using memorized tmpfile.""" self._assert_state("start", ("initialized",)) @@ -426,7 +426,7 @@ class FDCaptureBinary: self.tmpfile.seek(0) self.tmpfile.truncate() return res - + def done(self) -> None: """Stop capturing, restore streams, return original capture file, seeked to position zero.""" @@ -442,7 +442,7 @@ class FDCaptureBinary: self.syscapture.done() self.tmpfile.close() self._state = "done" - + def suspend(self) -> None: self._assert_state("suspend", ("started", "suspended")) if self._state == "suspended": @@ -469,11 +469,11 @@ class FDCapture(FDCaptureBinary): """Capture IO to/from a given OS-level file descriptor. snap() produces text. - """ - + """ + # Ignore type because it doesn't match the type in the superclass (bytes). EMPTY_BUFFER = "" # type: ignore - + def snap(self): self._assert_state("snap", ("started", "suspended")) self.tmpfile.seek(0) @@ -481,15 +481,15 @@ class FDCapture(FDCaptureBinary): self.tmpfile.seek(0) self.tmpfile.truncate() return res - + def writeorg(self, data): """Write to original file descriptor.""" super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream - - + + # MultiCapture - - + + # This class was a namedtuple, but due to mypy limitation[0] it could not be # made generic, so was replaced by a regular class which tries to emulate the # pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can @@ -499,40 +499,40 @@ class FDCapture(FDCaptureBinary): @functools.total_ordering class CaptureResult(Generic[AnyStr]): """The result of :method:`CaptureFixture.readouterr`.""" - + __slots__ = ("out", "err") - + def __init__(self, out: AnyStr, err: AnyStr) -> None: self.out: AnyStr = out self.err: AnyStr = err - + def __len__(self) -> int: return 2 - + def __iter__(self) -> Iterator[AnyStr]: return iter((self.out, self.err)) - + def __getitem__(self, item: int) -> AnyStr: return tuple(self)[item] - + def _replace( self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None ) -> "CaptureResult[AnyStr]": return CaptureResult( out=self.out if out is None else out, err=self.err if err is None else err ) - + def count(self, value: AnyStr) -> int: return tuple(self).count(value) - + def index(self, value) -> int: return tuple(self).index(value) - + def __eq__(self, other: object) -> bool: if not isinstance(other, (CaptureResult, tuple)): return NotImplemented return tuple(self) == tuple(other) - + def __hash__(self) -> int: return hash(tuple(self)) @@ -540,20 +540,20 @@ class CaptureResult(Generic[AnyStr]): if not isinstance(other, (CaptureResult, tuple)): return NotImplemented return tuple(self) < tuple(other) - + def __repr__(self) -> str: return f"CaptureResult(out={self.out!r}, err={self.err!r})" - - + + class MultiCapture(Generic[AnyStr]): _state = None _in_suspended = False - + def __init__(self, in_, out, err) -> None: self.in_ = in_ self.out = out self.err = err - + def __repr__(self) -> str: return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format( self.out, self.err, self.in_, self._state, self._in_suspended, @@ -561,54 +561,54 @@ class MultiCapture(Generic[AnyStr]): def start_capturing(self) -> None: self._state = "started" - if self.in_: - self.in_.start() - if self.out: - self.out.start() - if self.err: - self.err.start() - + if self.in_: + self.in_.start() + if self.out: + self.out.start() + if self.err: + self.err.start() + def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]: """Pop current snapshot out/err capture and flush to orig streams.""" - out, err = self.readouterr() - if out: - self.out.writeorg(out) - if err: - self.err.writeorg(err) - return out, err - + out, err = self.readouterr() + if out: + self.out.writeorg(out) + if err: + self.err.writeorg(err) + return out, err + def suspend_capturing(self, in_: bool = False) -> None: self._state = "suspended" - if self.out: - self.out.suspend() - if self.err: - self.err.suspend() - if in_ and self.in_: - self.in_.suspend() - self._in_suspended = True - + if self.out: + self.out.suspend() + if self.err: + self.err.suspend() + if in_ and self.in_: + self.in_.suspend() + self._in_suspended = True + def resume_capturing(self) -> None: self._state = "started" - if self.out: - self.out.resume() - if self.err: - self.err.resume() + if self.out: + self.out.resume() + if self.err: + self.err.resume() if self._in_suspended: - self.in_.resume() + self.in_.resume() self._in_suspended = False - + def stop_capturing(self) -> None: """Stop capturing and reset capturing streams.""" if self._state == "stopped": - raise ValueError("was already stopped") + raise ValueError("was already stopped") self._state = "stopped" - if self.out: - self.out.done() - if self.err: - self.err.done() - if self.in_: - self.in_.done() - + if self.out: + self.out.done() + if self.err: + self.err.done() + if self.in_: + self.in_.done() + def is_started(self) -> bool: """Whether actively capturing -- not suspended or stopped.""" return self._state == "started" @@ -623,8 +623,8 @@ class MultiCapture(Generic[AnyStr]): else: err = "" return CaptureResult(out, err) - - + + def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: if method == "fd": return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) @@ -637,88 +637,88 @@ def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True) ) raise ValueError(f"unknown capturing method: {method!r}") - - + + # CaptureManager and CaptureFixture - - + + class CaptureManager: """The 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 attached to the collection/runtest report. - + There are two levels of capture: - + * global: enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled during collection and each test 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: self._method = method self._global_capturing: Optional[MultiCapture[str]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None - + def __repr__(self) -> str: return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format( self._method, self._global_capturing, self._capture_fixture ) - + def is_capturing(self) -> Union[str, bool]: if self.is_globally_capturing(): return "global" if self._capture_fixture: return "fixture %s" % self._capture_fixture.request.fixturename return False - + # Global capturing control - + def is_globally_capturing(self) -> bool: return self._method != "no" - + def start_global_capturing(self) -> None: assert self._global_capturing is None self._global_capturing = _get_multicapture(self._method) self._global_capturing.start_capturing() - + def stop_global_capturing(self) -> None: if self._global_capturing is not None: self._global_capturing.pop_outerr_to_orig() self._global_capturing.stop_capturing() self._global_capturing = None - + def resume_global_capture(self) -> None: # During teardown of the python process, and on rare occasions, capture # attributes can be `None` while trying to resume global capture. if self._global_capturing is not None: self._global_capturing.resume_capturing() - + def suspend_global_capture(self, in_: bool = False) -> None: if self._global_capturing is not None: self._global_capturing.suspend_capturing(in_=in_) - + def suspend(self, in_: bool = False) -> None: # Need to undo local capsys-et-al if it exists before disabling global capture. self.suspend_fixture() self.suspend_global_capture(in_) - + def resume(self) -> None: self.resume_global_capture() self.resume_fixture() - + def read_global_capture(self) -> CaptureResult[str]: assert self._global_capturing is not None return self._global_capturing.readouterr() # Fixture Control - + def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None: if self._capture_fixture: current_fixture = self._capture_fixture.request.fixturename @@ -729,7 +729,7 @@ class CaptureManager: ) ) self._capture_fixture = capture_fixture - + def unset_fixture(self) -> None: self._capture_fixture = None @@ -738,22 +738,22 @@ class CaptureManager: them so they take precedence over the global capture.""" if self._capture_fixture: self._capture_fixture._start() - + def deactivate_fixture(self) -> None: """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any.""" if self._capture_fixture: self._capture_fixture.close() - + def suspend_fixture(self) -> None: if self._capture_fixture: self._capture_fixture._suspend() - + def resume_fixture(self) -> None: if self._capture_fixture: self._capture_fixture._resume() - + # Helper context managers - + @contextlib.contextmanager def global_and_fixture_disabled(self) -> Generator[None, None, None]: """Context manager to temporarily disable global and current fixture capturing.""" @@ -770,7 +770,7 @@ class CaptureManager: self.resume_global_capture() if do_fixture: self.resume_fixture() - + @contextlib.contextmanager def item_capture(self, when: str, item: Item) -> Generator[None, None, None]: self.resume_global_capture() @@ -780,13 +780,13 @@ class CaptureManager: finally: self.deactivate_fixture() self.suspend_global_capture(in_=False) - + out, err = self.read_global_capture() item.add_report_section(when, "stdout", out) item.add_report_section(when, "stderr", err) - + # Hooks - + @hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector: Collector): if isinstance(collector, File): @@ -801,12 +801,12 @@ class CaptureManager: rep.sections.append(("Captured stderr", err)) else: yield - + @hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: with self.item_capture("setup", item): yield - + @hookimpl(hookwrapper=True) def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: with self.item_capture("call", item): @@ -825,11 +825,11 @@ class CaptureManager: def pytest_internalerror(self) -> None: self.stop_global_capturing() - + class CaptureFixture(Generic[AnyStr]): """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`, :fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" - + def __init__( self, captureclass, request: SubRequest, *, _ispytest: bool = False ) -> None: @@ -839,14 +839,14 @@ class CaptureFixture(Generic[AnyStr]): self._capture: Optional[MultiCapture[AnyStr]] = None self._captured_out = self.captureclass.EMPTY_BUFFER self._captured_err = self.captureclass.EMPTY_BUFFER - + def _start(self) -> None: if self._capture is None: self._capture = MultiCapture( in_=None, out=self.captureclass(1), err=self.captureclass(2), ) self._capture.start_capturing() - + def close(self) -> None: if self._capture is not None: out, err = self._capture.pop_outerr_to_orig() @@ -854,11 +854,11 @@ class CaptureFixture(Generic[AnyStr]): self._captured_err += err self._capture.stop_capturing() self._capture = None - + def readouterr(self) -> CaptureResult[AnyStr]: """Read and return the captured output so far, resetting the internal buffer. - + :returns: The captured content as a namedtuple with ``out`` and ``err`` string attributes. @@ -871,42 +871,42 @@ class CaptureFixture(Generic[AnyStr]): self._captured_out = self.captureclass.EMPTY_BUFFER self._captured_err = self.captureclass.EMPTY_BUFFER return CaptureResult(captured_out, captured_err) - + def _suspend(self) -> None: """Suspend this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.suspend_capturing() - + def _resume(self) -> None: """Resume this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.resume_capturing() - + def _is_started(self) -> bool: """Whether actively capturing -- not disabled or closed.""" if self._capture is not None: return self._capture.is_started() return False - + @contextlib.contextmanager def disabled(self) -> Generator[None, None, None]: """Temporarily disable capturing while inside the ``with`` block.""" capmanager = self.request.config.pluginmanager.getplugin("capturemanager") with capmanager.global_and_fixture_disabled(): yield - - + + # The fixtures. - - + + @fixture def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. - + The captured output is made available via ``capsys.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. - """ + """ capman = request.config.pluginmanager.getplugin("capturemanager") capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True) capman.set_fixture(capture_fixture) @@ -914,8 +914,8 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: yield capture_fixture capture_fixture.close() capman.unset_fixture() - - + + @fixture def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. @@ -923,7 +923,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, The captured output is made available via ``capsysbinary.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``bytes`` objects. - """ + """ capman = request.config.pluginmanager.getplugin("capturemanager") capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True) capman.set_fixture(capture_fixture) @@ -931,16 +931,16 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, yield capture_fixture capture_fixture.close() capman.unset_fixture() - - + + @fixture def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: """Enable text capturing of writes to file descriptors ``1`` and ``2``. - + The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. - """ + """ capman = request.config.pluginmanager.getplugin("capturemanager") capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True) capman.set_fixture(capture_fixture) @@ -948,12 +948,12 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: yield capture_fixture capture_fixture.close() capman.unset_fixture() - - + + @fixture def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. - + The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``byte`` objects. diff --git a/contrib/python/pytest/py3/_pytest/compat.py b/contrib/python/pytest/py3/_pytest/compat.py index 6ed8e80daf..c23cc962ce 100644 --- a/contrib/python/pytest/py3/_pytest/compat.py +++ b/contrib/python/pytest/py3/_pytest/compat.py @@ -1,10 +1,10 @@ """Python version compatibility code.""" import enum -import functools -import inspect -import re -import sys -from contextlib import contextmanager +import functools +import inspect +import re +import sys +from contextlib import contextmanager from inspect import Parameter from inspect import signature from pathlib import Path @@ -16,21 +16,21 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - + import attr - -from _pytest.outcomes import fail -from _pytest.outcomes import TEST_OUTCOME - + +from _pytest.outcomes import fail +from _pytest.outcomes import TEST_OUTCOME + if TYPE_CHECKING: from typing import NoReturn from typing_extensions import Final - - + + _T = TypeVar("_T") _S = TypeVar("_S") - - + + # fmt: off # Singleton type for NOTSET, as described in: # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions @@ -38,7 +38,7 @@ class NotSetType(enum.Enum): token = 0 NOTSET: "Final" = NotSetType.token # noqa: E305 # fmt: on - + if sys.version_info >= (3, 8): from importlib import metadata as importlib_metadata else: @@ -46,27 +46,27 @@ else: def _format_args(func: Callable[..., Any]) -> str: - return str(signature(func)) - - -# The type of re.compile objects is not exposed in Python. -REGEX_TYPE = type(re.compile("")) - - + return str(signature(func)) + + +# The type of re.compile objects is not exposed in Python. +REGEX_TYPE = type(re.compile("")) + + def is_generator(func: object) -> bool: - genfunc = inspect.isgeneratorfunction(func) - return genfunc and not iscoroutinefunction(func) - - + genfunc = inspect.isgeneratorfunction(func) + return genfunc and not iscoroutinefunction(func) + + def iscoroutinefunction(func: object) -> bool: """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 @asyncio.coroutine. - + Note: copied and modified from Python 3.5's builtin couroutines.py to avoid importing asyncio directly, which in turns also initializes the "logging" module as a side-effect (see issue #8). - """ + """ return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) @@ -74,12 +74,12 @@ def is_async_function(func: object) -> bool: """Return True if the given function seems to be an async function or an async generator.""" return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) - - + + def getlocation(function, curdir: Optional[str] = None) -> str: - function = get_real_func(function) + function = get_real_func(function) fn = Path(inspect.getfile(function)) - lineno = function.__code__.co_firstlineno + lineno = function.__code__.co_firstlineno if curdir is not None: try: relfn = fn.relative_to(curdir) @@ -87,18 +87,18 @@ def getlocation(function, curdir: Optional[str] = None) -> str: pass else: return "%s:%d" % (relfn, lineno + 1) - return "%s:%d" % (fn, lineno + 1) - - + return "%s:%d" % (fn, lineno + 1) + + def num_mock_patch_args(function) -> int: """Return number of arguments used up by mock arguments (if any).""" - patchings = getattr(function, "patchings", None) - if not patchings: - return 0 - + patchings = getattr(function, "patchings", None) + if not patchings: + return 0 + mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object()) ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object()) - + return len( [ p @@ -117,66 +117,66 @@ def getfuncargnames( cls: Optional[type] = None, ) -> Tuple[str, ...]: """Return the names of a function's mandatory arguments. - + Should return the names of all function arguments that: * Aren't bound to an instance or type as in instance or class methods. * Don't have default values. * Aren't bound with functools.partial. * Aren't replaced with mocks. - - 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 - the case of cls, the function is a static method. - + + 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 + the case of cls, the function is a static method. + The name parameter should be the original name in which the function was collected. """ # TODO(RonnyPfannschmidt): This function should be refactored when we # revisit fixtures. The fixture mechanism should ask the node for # the fixture names, and not try to obtain directly from the # function object well after collection has occurred. - - # The parameters attribute of a Signature object contains an - # ordered mapping of parameter names to Parameter instances. This - # creates a tuple of the names of the parameters that don't have - # defaults. - try: - parameters = signature(function).parameters - except (ValueError, TypeError) as e: - fail( + + # The parameters attribute of a Signature object contains an + # ordered mapping of parameter names to Parameter instances. This + # creates a tuple of the names of the parameters that don't have + # defaults. + try: + parameters = signature(function).parameters + except (ValueError, TypeError) as e: + fail( f"Could not determine arguments of {function!r}: {e}", pytrace=False, - ) - - arg_names = tuple( - p.name - for p in parameters.values() - if ( - p.kind is Parameter.POSITIONAL_OR_KEYWORD - or p.kind is Parameter.KEYWORD_ONLY - ) - and p.default is Parameter.empty - ) + ) + + arg_names = tuple( + p.name + for p in parameters.values() + if ( + p.kind is Parameter.POSITIONAL_OR_KEYWORD + or p.kind is Parameter.KEYWORD_ONLY + ) + and p.default is Parameter.empty + ) if not name: name = function.__name__ - # If this function should be treated as a bound method even though - # it's passed as an unbound method or function, remove the first - # parameter name. - if is_method or ( + # If this function should be treated as a bound method even though + # it's passed as an unbound method or function, remove the first + # parameter name. + if is_method or ( cls and not isinstance(cls.__dict__.get(name, None), staticmethod) - ): - arg_names = arg_names[1:] - # Remove any names that will be replaced with mocks. - if hasattr(function, "__wrapped__"): - arg_names = arg_names[num_mock_patch_args(function) :] - return arg_names - - + ): + arg_names = arg_names[1:] + # Remove any names that will be replaced with mocks. + if hasattr(function, "__wrapped__"): + arg_names = arg_names[num_mock_patch_args(function) :] + return arg_names + + if sys.version_info < (3, 7): - + @contextmanager def nullcontext(): yield - + else: from contextlib import nullcontext as nullcontext # noqa: F401 @@ -186,14 +186,14 @@ def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: # Note: this code intentionally mirrors the code at the beginning of # getfuncargnames, to get the arguments which were excluded from its result # because they had default values. - return tuple( - p.name - for p in signature(function).parameters.values() - if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) - and p.default is not Parameter.empty - ) - - + return tuple( + p.name + for p in signature(function).parameters.values() + if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) + and p.default is not Parameter.empty + ) + + _non_printable_ascii_translate_table = { i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127) } @@ -207,23 +207,23 @@ def _translate_non_printable(s: str) -> str: STRING_TYPES = bytes, str - - + + def _bytes_to_ascii(val: bytes) -> str: return val.decode("ascii", "backslashreplace") - - + + def ascii_escaped(val: Union[bytes, str]) -> str: r"""If val is pure ASCII, return it as an str, otherwise, escape bytes objects into a sequence of escaped bytes: - + b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6' - + and escapes unicode objects into a sequence of escaped unicode ids, e.g.: - + r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944' - + Note: The obvious "v.decode('unicode-escape')" will return valid UTF-8 unicode if it finds them in bytes, but we @@ -235,93 +235,93 @@ def ascii_escaped(val: Union[bytes, str]) -> str: else: ret = val return ret - - + + @attr.s 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 when we are creating fixtures, because we wrap the function object ourselves with a decorator to issue warnings when the fixture function is called directly. - """ - + """ + obj = attr.ib() - - -def get_real_func(obj): + + +def get_real_func(obj): """Get the real function object of the (possibly) wrapped object by functools.wraps or functools.partial.""" - start_obj = obj - for i in range(100): - # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function - # to trigger a warning if it gets called directly instead of by pytest: we don't - # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) - new_obj = getattr(obj, "__pytest_wrapped__", None) - if isinstance(new_obj, _PytestWrapper): - obj = new_obj.obj - break - new_obj = getattr(obj, "__wrapped__", None) - if new_obj is None: - break - obj = new_obj - else: + start_obj = obj + for i in range(100): + # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function + # to trigger a warning if it gets called directly instead of by pytest: we don't + # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) + new_obj = getattr(obj, "__pytest_wrapped__", None) + if isinstance(new_obj, _PytestWrapper): + obj = new_obj.obj + break + new_obj = getattr(obj, "__wrapped__", None) + if new_obj is None: + break + obj = new_obj + else: from _pytest._io.saferepr import saferepr - raise ValueError( - ("could not find real function of {start}\nstopped at {current}").format( + raise ValueError( + ("could not find real function of {start}\nstopped at {current}").format( start=saferepr(start_obj), current=saferepr(obj) - ) - ) - if isinstance(obj, functools.partial): - obj = obj.func - return obj - - -def get_real_method(obj, holder): + ) + ) + if isinstance(obj, functools.partial): + obj = obj.func + return obj + + +def get_real_method(obj, holder): """Attempt to obtain the real function object that might be wrapping ``obj``, while at the same time returning a bound method to ``holder`` if the original object was a bound method.""" - try: - is_method = hasattr(obj, "__func__") - obj = get_real_func(obj) + try: + is_method = hasattr(obj, "__func__") + obj = get_real_func(obj) except Exception: # pragma: no cover - return obj - if is_method and hasattr(obj, "__get__") and callable(obj.__get__): - obj = obj.__get__(holder) - return obj - - -def getimfunc(func): - try: - return func.__func__ - except AttributeError: - return func - - + return obj + if is_method and hasattr(obj, "__get__") and callable(obj.__get__): + obj = obj.__get__(holder) + return obj + + +def getimfunc(func): + try: + return func.__func__ + except AttributeError: + return func + + def safe_getattr(object: Any, name: str, default: Any) -> Any: """Like getattr but return default upon any Exception or any OutcomeException. - - Attribute access can potentially fail for 'evil' Python objects. - See issue #214. + + Attribute access can potentially fail for 'evil' Python objects. + See issue #214. It catches OutcomeException because of #2490 (issue #580), new outcomes are derived from BaseException instead of Exception (for more details check #2707). - """ - try: - return getattr(object, name, default) - except TEST_OUTCOME: - return default - - + """ + try: + return getattr(object, name, default) + except TEST_OUTCOME: + return default + + def safe_isclass(obj: object) -> bool: - """Ignore any exception via isinstance on Python 3.""" - try: + """Ignore any exception via isinstance on Python 3.""" + try: return inspect.isclass(obj) - except Exception: - return False - - + except Exception: + return False + + if TYPE_CHECKING: if sys.version_info >= (3, 8): from typing import final as final @@ -330,24 +330,24 @@ if TYPE_CHECKING: elif sys.version_info >= (3, 8): from typing import final as final else: - + def final(f): return f - - + + if sys.version_info >= (3, 8): from functools import cached_property as cached_property else: from typing import overload from typing import Type - + class cached_property(Generic[_S, _T]): __slots__ = ("func", "__doc__") - + def __init__(self, func: Callable[[_S], _T]) -> None: self.func = func self.__doc__ = func.__doc__ - + @overload def __get__( self, instance: None, owner: Optional[Type[_S]] = ... diff --git a/contrib/python/pytest/py3/_pytest/config/__init__.py b/contrib/python/pytest/py3/_pytest/config/__init__.py index fb07b6bfb9..bd9e2883f9 100644 --- a/contrib/python/pytest/py3/_pytest/config/__init__.py +++ b/contrib/python/pytest/py3/_pytest/config/__init__.py @@ -1,16 +1,16 @@ """Command line options, ini-file and conftest.py processing.""" -import argparse +import argparse import collections.abc import contextlib -import copy +import copy import enum -import inspect -import os +import inspect +import os import re -import shlex -import sys -import types -import warnings +import shlex +import sys +import types +import warnings from functools import lru_cache from pathlib import Path from types import TracebackType @@ -30,32 +30,32 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union - + import attr -import py -from pluggy import HookimplMarker -from pluggy import HookspecMarker -from pluggy import PluginManager - -import _pytest._code +import py +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager + +import _pytest._code import _pytest.deprecated import _pytest.hookspec from .exceptions import PrintHelp as PrintHelp from .exceptions import UsageError as UsageError -from .findpaths import determine_setup -from _pytest._code import ExceptionInfo -from _pytest._code import filter_traceback +from .findpaths import determine_setup +from _pytest._code import ExceptionInfo +from _pytest._code import filter_traceback from _pytest._io import TerminalWriter from _pytest.compat import final from _pytest.compat import importlib_metadata from _pytest.outcomes import fail -from _pytest.outcomes import Skipped +from _pytest.outcomes import Skipped from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pathlib import ImportMode from _pytest.store import Store from _pytest.warning_types import PytestConfigWarning - + if TYPE_CHECKING: from _pytest._code.code import _TracebackStyle @@ -73,10 +73,10 @@ Ideally this type would be provided by pluggy itself. """ -hookimpl = HookimplMarker("pytest") -hookspec = HookspecMarker("pytest") - - +hookimpl = HookimplMarker("pytest") +hookspec = HookspecMarker("pytest") + + @final class ExitCode(enum.IntEnum): """Encodes the valid exit codes by pytest. @@ -100,30 +100,30 @@ class ExitCode(enum.IntEnum): NO_TESTS_COLLECTED = 5 -class ConftestImportFailure(Exception): +class ConftestImportFailure(Exception): def __init__( self, path: py.path.local, excinfo: Tuple[Type[Exception], Exception, TracebackType], ) -> None: super().__init__(path, excinfo) - self.path = path + self.path = path self.excinfo = excinfo - + def __str__(self) -> str: return "{}: {} (from {})".format( self.excinfo[0].__name__, self.excinfo[1], self.path ) - - + + def filter_traceback_for_conftest_import_failure( entry: _pytest._code.TracebackEntry, ) -> bool: """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 in _pytest.pathlib.import_path. - """ + """ return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) @@ -138,27 +138,27 @@ def main( :returns: An exit code. """ - try: - try: - config = _prepareconfig(args, plugins) - except ConftestImportFailure as e: - exc_info = ExceptionInfo(e.excinfo) + try: + try: + config = _prepareconfig(args, plugins) + except ConftestImportFailure as e: + exc_info = ExceptionInfo(e.excinfo) tw = TerminalWriter(sys.stderr) tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) exc_info.traceback = exc_info.traceback.filter( filter_traceback_for_conftest_import_failure - ) - exc_repr = ( - exc_info.getrepr(style="short", chain=False) - if exc_info.traceback - else exc_info.exconly() - ) + ) + exc_repr = ( + exc_info.getrepr(style="short", chain=False) + if exc_info.traceback + else exc_info.exconly() + ) formatted_tb = str(exc_repr) - for line in formatted_tb.splitlines(): - tw.line(line.rstrip(), red=True) + for line in formatted_tb.splitlines(): + tw.line(line.rstrip(), red=True) return ExitCode.USAGE_ERROR - else: - try: + else: + try: ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main( config=config ) @@ -166,15 +166,15 @@ def main( return ExitCode(ret) except ValueError: return ret - finally: - config._ensure_unconfigure() - except UsageError as e: + finally: + config._ensure_unconfigure() + except UsageError as e: tw = TerminalWriter(sys.stderr) - for msg in e.args: + for msg in e.args: tw.line(f"ERROR: {msg}\n", red=True) return ExitCode.USAGE_ERROR - - + + def console_main() -> int: """The CLI entry point of pytest. @@ -194,78 +194,78 @@ def console_main() -> int: class cmdline: # compatibility namespace - main = staticmethod(main) - - + main = staticmethod(main) + + def filename_arg(path: str, optname: str) -> str: """Argparse type validator for filename arguments. - + :path: Path of filename. :optname: Name of the option. - """ - if os.path.isdir(path): + """ + if os.path.isdir(path): raise UsageError(f"{optname} must be a filename, given: {path}") - return path - - + return path + + def directory_arg(path: str, optname: str) -> str: - """Argparse type validator for directory arguments. - + """Argparse type validator for directory arguments. + :path: Path of directory. :optname: Name of the option. - """ - if not os.path.isdir(path): + """ + if not os.path.isdir(path): raise UsageError(f"{optname} must be a directory, given: {path}") - return path - - + return path + + # Plugins that cannot be disabled via "-p no:X" currently. essential_plugins = ( - "mark", - "main", - "runner", + "mark", + "main", + "runner", "fixtures", "helpconfig", # Provides -p. ) default_plugins = essential_plugins + ( - "python", + "python", "terminal", - "debugging", - "unittest", - "capture", - "skipping", - "tmpdir", - "monkeypatch", - "recwarn", - "pastebin", - "nose", - "assertion", - "junitxml", - "doctest", - "cacheprovider", - "freeze_support", - "setuponly", - "setupplan", - "stepwise", - "warnings", - "logging", + "debugging", + "unittest", + "capture", + "skipping", + "tmpdir", + "monkeypatch", + "recwarn", + "pastebin", + "nose", + "assertion", + "junitxml", + "doctest", + "cacheprovider", + "freeze_support", + "setuponly", + "setupplan", + "stepwise", + "warnings", + "logging", "reports", *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), "faulthandler", -) - -builtin_plugins = set(default_plugins) -builtin_plugins.add("pytester") +) + +builtin_plugins = set(default_plugins) +builtin_plugins.add("pytester") builtin_plugins.add("pytester_assertions") - - + + def get_config( args: Optional[List[str]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> "Config": - # subsequent calls to main will create a fresh instance - pluginmanager = PytestPluginManager() + # subsequent calls to main will create a fresh instance + pluginmanager = PytestPluginManager() config = Config( pluginmanager, invocation_params=Config.InvocationParams( @@ -277,66 +277,66 @@ def get_config( # Handle any "-p no:plugin" args. pluginmanager.consider_preparse(args, exclude_only=True) - for spec in default_plugins: - pluginmanager.import_plugin(spec) + for spec in default_plugins: + pluginmanager.import_plugin(spec) + + return config + - return config - - def get_plugin_manager() -> "PytestPluginManager": """Obtain a new instance of the - :py:class:`_pytest.config.PytestPluginManager`, with default plugins - already loaded. - - This function can be used by integration with other tools, like hooking - into pytest to run tests into an IDE. - """ - return get_config().pluginmanager - - + :py:class:`_pytest.config.PytestPluginManager`, with default plugins + already loaded. + + This function can be used by integration with other tools, like hooking + into pytest to run tests into an IDE. + """ + return get_config().pluginmanager + + def _prepareconfig( args: Optional[Union[py.path.local, List[str]]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> "Config": - if args is None: - args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] + if args is None: + args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] elif not isinstance(args, list): msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) - + config = get_config(args, plugins) - pluginmanager = config.pluginmanager - try: - if plugins: - for plugin in plugins: + pluginmanager = config.pluginmanager + try: + if plugins: + for plugin in plugins: if isinstance(plugin, str): - pluginmanager.consider_pluginarg(plugin) - else: - pluginmanager.register(plugin) + pluginmanager.consider_pluginarg(plugin) + else: + pluginmanager.register(plugin) config = pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args - ) + pluginmanager=pluginmanager, args=args + ) return config - except BaseException: - config._ensure_unconfigure() - raise - - + except BaseException: + config._ensure_unconfigure() + raise + + @final -class PytestPluginManager(PluginManager): +class PytestPluginManager(PluginManager): """A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with additional pytest-specific functionality: - + * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and ``pytest_plugins`` global variables found in plugins being loaded. * ``conftest.py`` loading during start-up. - """ - + """ + def __init__(self) -> None: import _pytest.assertion - + super().__init__("pytest") # The objects are module objects, only used generically. self._conftest_plugins: Set[types.ModuleType] = set() @@ -345,9 +345,9 @@ class PytestPluginManager(PluginManager): self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {} self._conftestpath2mod: Dict[Path, types.ModuleType] = {} self._confcutdir: Optional[py.path.local] = None - self._noconftest = False + self._noconftest = False self._duplicatepaths: Set[py.path.local] = set() - + # plugins that were explicitly skipped with pytest.skip # list of (module name, skip reason) # previously we would issue a warning when a plugin was skipped, but @@ -355,71 +355,71 @@ class PytestPluginManager(PluginManager): # just stored here to be used later. self.skipped_plugins: List[Tuple[str, str]] = [] - self.add_hookspecs(_pytest.hookspec) - self.register(self) - if os.environ.get("PYTEST_DEBUG"): + self.add_hookspecs(_pytest.hookspec) + self.register(self) + if os.environ.get("PYTEST_DEBUG"): err: IO[str] = sys.stderr encoding: str = getattr(err, "encoding", "utf8") - try: + try: err = open( os.dup(err.fileno()), mode=err.mode, buffering=1, encoding=encoding, ) - except Exception: - pass - self.trace.root.setwriter(err.write) - self.enable_tracing() - - # Config._consider_importhook will set a real object if required. - self.rewrite_hook = _pytest.assertion.DummyRewriteHook() + except Exception: + pass + self.trace.root.setwriter(err.write) + self.enable_tracing() + + # Config._consider_importhook will set a real object if required. + self.rewrite_hook = _pytest.assertion.DummyRewriteHook() # 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): # 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). - if not name.startswith("pytest_"): - return + if not name.startswith("pytest_"): + return # Ignore names which can not be hooks. if name == "pytest_plugins": - return - - method = getattr(plugin, name) + return + + method = getattr(plugin, name) opts = super().parse_hookimpl_opts(plugin, name) - + # Consider only actual functions for hooks (#3775). - if not inspect.isroutine(method): - return - + if not inspect.isroutine(method): + return + # Collect unmarked hooks as long as they have the `pytest_' prefix. - if opts is None and name.startswith("pytest_"): - opts = {} + if opts is None and name.startswith("pytest_"): + opts = {} if opts is not None: # TODO: DeprecationWarning, people should use hookimpl # https://github.com/pytest-dev/pytest/issues/4562 known_marks = {m.name for m in getattr(method, "pytestmark", [])} - - for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"): + + for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"): opts.setdefault(name, hasattr(method, name) or name in known_marks) - return opts - + return opts + def parse_hookspec_opts(self, module_or_class, name: str): opts = super().parse_hookspec_opts(module_or_class, name) - if opts is None: - method = getattr(module_or_class, name) + if opts is None: + method = getattr(module_or_class, name) - if name.startswith("pytest_"): + if name.startswith("pytest_"): # todo: deprecate hookspec hacks # https://github.com/pytest-dev/pytest/issues/4562 known_marks = {m.name for m in getattr(method, "pytestmark", [])} - opts = { + opts = { "firstresult": hasattr(method, "firstresult") or "firstresult" in known_marks, "historic": hasattr(method, "historic") or "historic" in known_marks, - } - return opts - + } + return opts + def register( self, plugin: _PluggyPlugin, name: Optional[str] = None ) -> Optional[str]: @@ -430,47 +430,47 @@ class PytestPluginManager(PluginManager): "please remove it from your requirements.".format( name.replace("_", "-") ) - ) - ) + ) + ) return None ret: Optional[str] = super().register(plugin, name) - if ret: - self.hook.pytest_plugin_registered.call_historic( - kwargs=dict(plugin=plugin, manager=self) - ) - - if isinstance(plugin, types.ModuleType): - self.consider_module(plugin) - return ret - + if ret: + self.hook.pytest_plugin_registered.call_historic( + kwargs=dict(plugin=plugin, manager=self) + ) + + if isinstance(plugin, types.ModuleType): + self.consider_module(plugin) + return ret + def getplugin(self, name: str): # Support deprecated naming because plugins (xdist e.g.) use it. plugin: Optional[_PluggyPlugin] = self.get_plugin(name) return plugin - + def hasplugin(self, name: str) -> bool: """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: """: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. - config.addinivalue_line( - "markers", - "tryfirst: mark a hook implementation function such that the " - "plugin machinery will try to call it first/as early as possible.", - ) - config.addinivalue_line( - "markers", - "trylast: mark a hook implementation function such that the " - "plugin machinery will try to call it last/as late as possible.", - ) - self._configured = True - - # + config.addinivalue_line( + "markers", + "tryfirst: mark a hook implementation function such that the " + "plugin machinery will try to call it first/as early as possible.", + ) + config.addinivalue_line( + "markers", + "trylast: mark a hook implementation function such that the " + "plugin machinery will try to call it last/as late as possible.", + ) + self._configured = True + + # # Internal API for local conftest plugin handling. - # + # def _set_initial_conftests(self, namespace: argparse.Namespace) -> None: """Load initial conftest files given a preparsed "namespace". @@ -478,77 +478,77 @@ class PytestPluginManager(PluginManager): arguments ('--my-opt somepath') we might get some false positives. All builtin and 3rd party plugins will have been loaded, however, so common options will not confuse our logic here. - """ - current = py.path.local() - self._confcutdir = ( - current.join(namespace.confcutdir, abs=True) - if namespace.confcutdir - else None - ) - self._noconftest = namespace.noconftest - self._using_pyargs = namespace.pyargs - testpaths = namespace.file_or_dir - foundanchor = False + """ + current = py.path.local() + self._confcutdir = ( + current.join(namespace.confcutdir, abs=True) + if namespace.confcutdir + else None + ) + self._noconftest = namespace.noconftest + self._using_pyargs = namespace.pyargs + testpaths = namespace.file_or_dir + foundanchor = False for testpath in testpaths: path = str(testpath) - # remove node-id syntax - i = path.find("::") - if i != -1: - path = path[:i] - anchor = current.join(path, abs=1) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = current.join(path, abs=1) if anchor.exists(): # we found some file object self._try_load_conftest(anchor, namespace.importmode) - foundanchor = True - if not foundanchor: + foundanchor = True + if not foundanchor: self._try_load_conftest(current, namespace.importmode) - + def _try_load_conftest( self, anchor: py.path.local, importmode: Union[str, ImportMode] ) -> None: self._getconftestmodules(anchor, importmode) - # let's also consider test* subdirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): + # let's also consider test* subdirs + if anchor.check(dir=1): + for x in anchor.listdir("test*"): + if x.check(dir=1): self._getconftestmodules(x, importmode) - - @lru_cache(maxsize=128) + + @lru_cache(maxsize=128) def _getconftestmodules( self, path: py.path.local, importmode: Union[str, ImportMode], ) -> List[types.ModuleType]: - if self._noconftest: - return [] - - if path.isfile(): - directory = path.dirpath() - else: - directory = path - + if self._noconftest: + return [] + + if path.isfile(): + directory = path.dirpath() + else: + directory = path + # XXX these days we may rather want to use config.rootpath - # 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. - clist = [] + clist = [] for parent in directory.parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.isfile(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.isfile(): mod = self._importconftest(conftestpath, importmode) - clist.append(mod) - self._dirpath2confmods[directory] = clist - return clist - + clist.append(mod) + self._dirpath2confmods[directory] = clist + return clist + def _rget_with_confmod( self, name: str, path: py.path.local, importmode: Union[str, ImportMode], ) -> Tuple[types.ModuleType, Any]: modules = self._getconftestmodules(path, importmode) - for mod in reversed(modules): - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + def _importconftest( self, conftestpath: py.path.local, importmode: Union[str, ImportMode], ) -> types.ModuleType: @@ -561,11 +561,11 @@ class PytestPluginManager(PluginManager): with contextlib.suppress(KeyError): return self._conftestpath2mod[key] - + pkgpath = conftestpath.pypkgpath() if pkgpath is None: _ensure_removed_sysmodule(conftestpath.purebasename) - + try: mod = import_path(conftestpath, mode=importmode) except Exception as e: @@ -606,11 +606,11 @@ class PytestPluginManager(PluginManager): ) fail(msg.format(conftestpath, self._confcutdir), pytrace=False) - # - # API for bootstrapping plugin loading - # - # - + # + # API for bootstrapping plugin loading + # + # + def consider_preparse( self, args: Sequence[str], *, exclude_only: bool = False ) -> None: @@ -633,22 +633,22 @@ class PytestPluginManager(PluginManager): if exclude_only and not parg.startswith("no:"): continue self.consider_pluginarg(parg) - + def consider_pluginarg(self, arg: str) -> None: - if arg.startswith("no:"): - name = arg[3:] + if arg.startswith("no:"): + name = arg[3:] if name in essential_plugins: raise UsageError("plugin %s cannot be disabled" % name) # PR #4304: remove stepwise if cacheprovider is blocked. - if name == "cacheprovider": - self.set_blocked("stepwise") - self.set_blocked("pytest_stepwise") - - self.set_blocked(name) - if not name.startswith("pytest_"): - self.set_blocked("pytest_" + name) - else: + if name == "cacheprovider": + self.set_blocked("stepwise") + self.set_blocked("pytest_stepwise") + + self.set_blocked(name) + if not name.startswith("pytest_"): + self.set_blocked("pytest_" + name) + else: name = arg # Unblock the plugin. None indicates that it has been blocked. # There is no interface with pluggy for this. @@ -658,23 +658,23 @@ class PytestPluginManager(PluginManager): if self._name2plugin.get("pytest_" + name, -1) is None: del self._name2plugin["pytest_" + name] self.import_plugin(arg, consider_entry_points=True) - + def consider_conftest(self, conftestmodule: types.ModuleType) -> None: - self.register(conftestmodule, name=conftestmodule.__file__) - + self.register(conftestmodule, name=conftestmodule.__file__) + def consider_env(self) -> None: - self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) - + self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) + def consider_module(self, mod: types.ModuleType) -> None: - self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) - + self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) + def _import_plugin_specs( self, spec: Union[None, types.ModuleType, str, Sequence[str]] ) -> None: - plugins = _get_plugin_specs_as_list(spec) - for import_spec in plugins: - self.import_plugin(import_spec) - + plugins = _get_plugin_specs_as_list(spec) + for import_spec in plugins: + self.import_plugin(import_spec) + def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None: """Import a plugin with ``modname``. @@ -682,37 +682,37 @@ class PytestPluginManager(PluginManager): considered to find a plugin. """ # Most often modname refers to builtin modules, e.g. "pytester", - # "terminal" or "capture". Those plugins are registered under their - # basename for historic purposes but must be imported with the - # _pytest prefix. + # "terminal" or "capture". Those plugins are registered under their + # basename for historic purposes but must be imported with the + # _pytest prefix. assert isinstance(modname, str), ( - "module name as text required, got %r" % modname - ) - if self.is_blocked(modname) or self.get_plugin(modname) is not None: - return + "module name as text required, got %r" % modname + ) + if self.is_blocked(modname) or self.get_plugin(modname) is not None: + return importspec = "_pytest." + modname if modname in builtin_plugins else modname - self.rewrite_hook.mark_rewrite(importspec) + self.rewrite_hook.mark_rewrite(importspec) if consider_entry_points: loaded = self.load_setuptools_entrypoints("pytest11", name=modname) if loaded: return - try: - __import__(importspec) - except ImportError as e: + try: + __import__(importspec) + except ImportError as e: raise ImportError( 'Error importing plugin "{}": {}'.format(modname, str(e.args[0])) ).with_traceback(e.__traceback__) from e - - except Skipped as e: + + except Skipped as e: self.skipped_plugins.append((modname, e.msg or "")) - else: - mod = sys.modules[importspec] - self.register(mod, modname) - - + else: + mod = sys.modules[importspec] + self.register(mod, modname) + + def _get_plugin_specs_as_list( specs: Union[None, types.ModuleType, str, Sequence[str]] ) -> List[str]: @@ -728,28 +728,28 @@ def _get_plugin_specs_as_list( return specs.split(",") if specs else [] # Direct specification. if isinstance(specs, collections.abc.Sequence): - return list(specs) + return list(specs) raise UsageError( "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r" % specs ) - - + + def _ensure_removed_sysmodule(modname: str) -> None: - try: - del sys.modules[modname] - except KeyError: - pass - - + try: + del sys.modules[modname] + except KeyError: + pass + + class Notset: - def __repr__(self): - return "<NOTSET>" - - -notset = Notset() - - + def __repr__(self): + return "<NOTSET>" + + +notset = Notset() + + 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 be marked for assertion rewrite. @@ -784,20 +784,20 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: """ package_files = list(package_files) seen_some = False - for fn in package_files: - is_simple_module = "/" not in fn and fn.endswith(".py") - is_package = fn.count("/") == 1 and fn.endswith("__init__.py") - if is_simple_module: - module_name, _ = os.path.splitext(fn) + for fn in package_files: + is_simple_module = "/" not in fn and fn.endswith(".py") + is_package = fn.count("/") == 1 and fn.endswith("__init__.py") + if is_simple_module: + module_name, _ = os.path.splitext(fn) # we ignore "setup.py" at the root of the distribution if module_name != "setup": seen_some = True yield module_name - elif is_package: - package_name = os.path.dirname(fn) + elif is_package: + package_name = os.path.dirname(fn) seen_some = True - yield package_name - + yield package_name + if not seen_some: # 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 @@ -812,11 +812,11 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: new_package_files.append(new_fn) if new_package_files: yield from _iter_rewritable_modules(new_package_files) - + def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: return tuple(args) - + @final class Config: @@ -875,7 +875,7 @@ class Config: args=(), plugins=None, dir=Path.cwd() ) - self.option = argparse.Namespace() + self.option = argparse.Namespace() """Access to command line option as attributes. :type: argparse.Namespace @@ -887,19 +887,19 @@ class Config: :type: InvocationParams """ - _a = FILE_OR_DIR - self._parser = Parser( + _a = FILE_OR_DIR + self._parser = Parser( usage=f"%(prog)s [options] [{_a}] [{_a}] [...]", - processopt=self._processopt, - ) - self.pluginmanager = pluginmanager + processopt=self._processopt, + ) + self.pluginmanager = pluginmanager """The plugin manager handles plugin registration and hook invocation. :type: PytestPluginManager """ - self.trace = self.pluginmanager.trace.root.get("config") - self.hook = self.pluginmanager.hook + self.trace = self.pluginmanager.trace.root.get("config") + self.hook = self.pluginmanager.hook self._inicache: Dict[str, Any] = {} self._override_ini: Sequence[str] = () self._opt2dest: Dict[str, str] = {} @@ -907,12 +907,12 @@ class Config: # A place where plugins can store information on the config for their # own use. Currently only intended for internal plugins. self._store = Store() - self.pluginmanager.register(self, "pytestconfig") - self._configured = False + self.pluginmanager.register(self, "pytestconfig") + self._configured = False self.hook.pytest_addoption.call_historic( kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) ) - + if TYPE_CHECKING: from _pytest.cacheprovider import Cache @@ -928,7 +928,7 @@ class Config: :type: py.path.local """ return py.path.local(str(self.invocation_params.dir)) - + @property def rootpath(self) -> Path: """The path to the :ref:`rootdir <rootdir>`. @@ -971,44 +971,44 @@ class Config: def add_cleanup(self, func: Callable[[], None]) -> None: """Add a function to be called when the config object gets out of - use (usually coninciding with pytest_unconfigure).""" - self._cleanup.append(func) - + use (usually coninciding with pytest_unconfigure).""" + self._cleanup.append(func) + def _do_configure(self) -> None: - assert not self._configured - self._configured = True + assert not self._configured + self._configured = True with warnings.catch_warnings(): warnings.simplefilter("default") self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) - + def _ensure_unconfigure(self) -> None: - if self._configured: - self._configured = False - self.hook.pytest_unconfigure(config=self) - self.hook.pytest_configure._call_history = [] - while self._cleanup: - fin = self._cleanup.pop() - fin() - + if self._configured: + self._configured = False + self.hook.pytest_unconfigure(config=self) + self.hook.pytest_configure._call_history = [] + while self._cleanup: + fin = self._cleanup.pop() + fin() + def get_terminal_writer(self) -> TerminalWriter: terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( "terminalreporter" ) return terminalreporter._tw - + def pytest_cmdline_parse( self, pluginmanager: PytestPluginManager, args: List[str] ) -> "Config": try: self.parse(args) except UsageError: - + # Handle --version and --help here in a minimal fashion. # This gets done via helpconfig normally, but its # pytest_cmdline_main is not called in case of errors. if getattr(self.option, "version", False) or "--version" in args: from _pytest.helpconfig import showversion - + showversion(self) elif ( getattr(self.option, "help", False) or "--help" in args or "-h" in args @@ -1017,11 +1017,11 @@ class Config: sys.stdout.write( "\nNOTE: displaying only minimal help due to UsageError.\n\n" ) - + raise - - return self - + + return self + def notify_exception( self, excinfo: ExceptionInfo[BaseException], @@ -1029,111 +1029,111 @@ class Config: ) -> None: if option and getattr(option, "fulltrace", False): style: _TracebackStyle = "long" - else: - style = "native" - excrepr = excinfo.getrepr( - funcargs=True, showlocals=getattr(option, "showlocals", False), style=style - ) - res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) - if not any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" % line) - sys.stderr.flush() - + else: + style = "native" + excrepr = excinfo.getrepr( + funcargs=True, showlocals=getattr(option, "showlocals", False), style=style + ) + res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) + if not any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" % line) + sys.stderr.flush() + def cwd_relative_nodeid(self, nodeid: str) -> str: # nodeid's are relative to the rootpath, compute relative to cwd. if self.invocation_params.dir != self.rootpath: fullpath = self.rootpath / nodeid nodeid = bestrelpath(self.invocation_params.dir, fullpath) - return nodeid - - @classmethod + return nodeid + + @classmethod def fromdictargs(cls, option_dict, args) -> "Config": """Constructor usable for subprocesses.""" config = get_config(args) - config.option.__dict__.update(option_dict) - config.parse(args, addopts=False) - for x in config.option.plugins: - config.pluginmanager.consider_pluginarg(x) - return config - + config.option.__dict__.update(option_dict) + config.parse(args, addopts=False) + for x in config.option.plugins: + config.pluginmanager.consider_pluginarg(x) + return config + def _processopt(self, opt: "Argument") -> None: - for name in opt._short_opts + opt._long_opts: - self._opt2dest[name] = opt.dest - + for name in opt._short_opts + opt._long_opts: + self._opt2dest[name] = opt.dest + if hasattr(opt, "default"): - if not hasattr(self.option, opt.dest): - setattr(self.option, opt.dest, opt.default) - - @hookimpl(trylast=True) + if not hasattr(self.option, opt.dest): + setattr(self.option, opt.dest, opt.default) + + @hookimpl(trylast=True) def pytest_load_initial_conftests(self, early_config: "Config") -> None: - self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - + self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) + def _initini(self, args: Sequence[str]) -> None: - ns, unknown_args = self._parser.parse_known_and_unknown_args( - args, namespace=copy.copy(self.option) - ) + ns, unknown_args = self._parser.parse_known_and_unknown_args( + args, namespace=copy.copy(self.option) + ) rootpath, inipath, inicfg = determine_setup( - ns.inifilename, - ns.file_or_dir + unknown_args, - rootdir_cmd_arg=ns.rootdir or None, - config=self, - ) + ns.inifilename, + ns.file_or_dir + unknown_args, + rootdir_cmd_arg=ns.rootdir or None, + config=self, + ) self._rootpath = rootpath self._inipath = inipath self.inicfg = inicfg self._parser.extra_info["rootdir"] = str(self.rootpath) self._parser.extra_info["inifile"] = str(self.inipath) - self._parser.addini("addopts", "extra command line options", "args") - self._parser.addini("minversion", "minimally required pytest version") + self._parser.addini("addopts", "extra command line options", "args") + self._parser.addini("minversion", "minimally required pytest version") self._parser.addini( "required_plugins", "plugins that must be present for pytest to run", type="args", default=[], ) - self._override_ini = ns.override_ini or () - + self._override_ini = ns.override_ini or () + def _consider_importhook(self, args: Sequence[str]) -> None: - """Install the PEP 302 import hook if using assertion rewriting. - - Needs to parse the --assert=<mode> option from the commandline - and find all the installed plugins to mark them for rewriting - by the importhook. - """ - ns, unknown_args = self._parser.parse_known_and_unknown_args(args) + """Install the PEP 302 import hook if using assertion rewriting. + + Needs to parse the --assert=<mode> option from the commandline + and find all the installed plugins to mark them for rewriting + by the importhook. + """ + ns, unknown_args = self._parser.parse_known_and_unknown_args(args) mode = getattr(ns, "assertmode", "plain") - if mode == "rewrite": + if mode == "rewrite": import _pytest.assertion - try: - hook = _pytest.assertion.install_importhook(self) - except SystemError: - mode = "plain" - else: - self._mark_plugins_for_rewrite(hook) + try: + hook = _pytest.assertion.install_importhook(self) + except SystemError: + mode = "plain" + else: + self._mark_plugins_for_rewrite(hook) self._warn_about_missing_assertion(mode) - + def _mark_plugins_for_rewrite(self, hook) -> None: """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.""" - self.pluginmanager.rewrite_hook = hook - - if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): - # We don't autoload from setuptools entry points, no need to continue. - return - - package_files = ( + self.pluginmanager.rewrite_hook = hook + + if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): + # We don't autoload from setuptools entry points, no need to continue. + return + + package_files = ( str(file) for dist in importlib_metadata.distributions() if any(ep.group == "pytest11" for ep in dist.entry_points) for file in dist.files or [] - ) - - for name in _iter_rewritable_modules(package_files): - hook.mark_rewrite(name) - + ) + + for name in _iter_rewritable_modules(package_files): + hook.mark_rewrite(name) + def _validate_args(self, args: List[str], via: str) -> List[str]: """Validate known args.""" self._parser._config_source_hint = via # type: ignore @@ -1147,15 +1147,15 @@ class Config: return args def _preparse(self, args: List[str], addopts: bool = True) -> None: - if addopts: + if addopts: env_addopts = os.environ.get("PYTEST_ADDOPTS", "") if len(env_addopts): args[:] = ( self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS") + args ) - self._initini(args) - if addopts: + self._initini(args) + if addopts: args[:] = ( self._validate_args(self.getini("addopts"), "via addopts config") + args ) @@ -1163,18 +1163,18 @@ class Config: self.known_args_namespace = self._parser.parse_known_args( args, namespace=copy.copy(self.option) ) - self._checkversion() - self._consider_importhook(args) + self._checkversion() + self._consider_importhook(args) self.pluginmanager.consider_preparse(args, exclude_only=False) - if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): - # Don't autoload from setuptools entry point. Only explicitly specified - # plugins are going to be loaded. - self.pluginmanager.load_setuptools_entrypoints("pytest11") - self.pluginmanager.consider_env() + if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): + # Don't autoload from setuptools entry point. Only explicitly specified + # plugins are going to be loaded. + self.pluginmanager.load_setuptools_entrypoints("pytest11") + self.pluginmanager.consider_env() self.known_args_namespace = self._parser.parse_known_args( args, namespace=copy.copy(self.known_args_namespace) - ) + ) self._validate_plugins() self._warn_about_skipped_plugins() @@ -1186,22 +1186,22 @@ class Config: if self.known_args_namespace.confcutdir is None and self.inipath is not None: confcutdir = str(self.inipath.parent) - self.known_args_namespace.confcutdir = confcutdir - try: - self.hook.pytest_load_initial_conftests( - early_config=self, args=args, parser=self._parser - ) + self.known_args_namespace.confcutdir = confcutdir + try: + self.hook.pytest_load_initial_conftests( + early_config=self, args=args, parser=self._parser + ) except ConftestImportFailure as e: if self.known_args_namespace.help or self.known_args_namespace.version: - # we don't want to prevent --help/--version to work - # so just let is pass and print a warning at the end + # we don't want to prevent --help/--version to work + # so just let is pass and print a warning at the end self.issue_config_time_warning( PytestConfigWarning(f"could not load initial conftests: {e.path}"), stacklevel=2, ) - else: - raise - + else: + raise + @hookimpl(hookwrapper=True) def pytest_collection(self) -> Generator[None, None, None]: """Validate invalid ini keys after collection is done so we take in account @@ -1210,10 +1210,10 @@ class Config: self._validate_config_options() def _checkversion(self) -> None: - import pytest - - minver = self.inicfg.get("minversion", None) - if minver: + import pytest + + minver = self.inicfg.get("minversion", None) + if minver: # Imported lazily to improve start-up time. from packaging.version import Version @@ -1223,11 +1223,11 @@ class Config: ) if Version(minver) > Version(pytest.__version__): - raise pytest.UsageError( + raise pytest.UsageError( "%s: 'minversion' requires pytest-%s, actual pytest-%s'" % (self.inipath, minver, pytest.__version__,) - ) - + ) + def _validate_config_options(self) -> None: for key in sorted(self._get_unknown_ini_keys()): self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") @@ -1274,29 +1274,29 @@ class Config: def parse(self, args: List[str], addopts: bool = True) -> None: # Parse given cmdline arguments into this config object. - assert not hasattr( - self, "args" - ), "can only parse cmdline args at most once per Config object" - self.hook.pytest_addhooks.call_historic( - kwargs=dict(pluginmanager=self.pluginmanager) - ) - self._preparse(args, addopts=addopts) - # XXX deprecated hook: - self.hook.pytest_cmdline_preparse(config=self, args=args) + assert not hasattr( + self, "args" + ), "can only parse cmdline args at most once per Config object" + self.hook.pytest_addhooks.call_historic( + kwargs=dict(pluginmanager=self.pluginmanager) + ) + self._preparse(args, addopts=addopts) + # XXX deprecated hook: + self.hook.pytest_cmdline_preparse(config=self, args=args) self._parser.after_preparse = True # type: ignore - try: - args = self._parser.parse_setoption( - args, self.option, namespace=self.option - ) - if not args: + try: + args = self._parser.parse_setoption( + args, self.option, namespace=self.option + ) + if not args: if self.invocation_params.dir == self.rootpath: - args = self.getini("testpaths") - if not args: + args = self.getini("testpaths") + if not args: args = [str(self.invocation_params.dir)] - self.args = args - except PrintHelp: - pass - + self.args = args + except PrintHelp: + pass + def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None: """Issue and handle a warning during the "configure" stage. @@ -1344,10 +1344,10 @@ class Config: """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 first line in its value.""" - x = self.getini(name) - assert isinstance(x, list) - x.append(line) # modifies the cached list inline - + x = self.getini(name) + assert isinstance(x, list) + x.append(line) # modifies the cached list inline + def getini(self, name: str): """Return configuration value from an :ref:`ini file <configfiles>`. @@ -1355,27 +1355,27 @@ class Config: :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` call (usually from a plugin), a ValueError is raised. """ - try: - return self._inicache[name] - except KeyError: - self._inicache[name] = val = self._getini(name) - return val - + try: + return self._inicache[name] + except KeyError: + self._inicache[name] = val = self._getini(name) + return val + def _getini(self, name: str): - try: - description, type, default = self._parser._inidict[name] + try: + description, type, default = self._parser._inidict[name] except KeyError as e: raise ValueError(f"unknown configuration value: {name!r}") from e override_value = self._get_override_ini_value(name) if override_value is None: - try: - value = self.inicfg[name] - except KeyError: - if default is not None: - return default - if type is None: - return "" - return [] + try: + value = self.inicfg[name] + except KeyError: + if default is not None: + return default + if type is None: + return "" + return [] else: value = override_value # Coerce the values based on types. @@ -1393,94 +1393,94 @@ class Config: # a_line_list = ["tests", "acceptance"] # 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. assert self.inipath is not None dp = self.inipath.parent input_values = shlex.split(value) if isinstance(value, str) else value return [py.path.local(str(dp / x)) for x in input_values] - elif type == "args": + elif type == "args": return shlex.split(value) if isinstance(value, str) else value - elif type == "linelist": + elif type == "linelist": if isinstance(value, str): return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] else: return value - elif type == "bool": + elif type == "bool": return _strtobool(str(value).strip()) - else: + else: assert type in [None, "string"] - return value - + return value + def _getconftest_pathlist( self, name: str, path: py.path.local ) -> Optional[List[py.path.local]]: - try: + try: mod, relroots = self.pluginmanager._rget_with_confmod( name, path, self.getoption("importmode") ) - except KeyError: - return None - modpath = py.path.local(mod.__file__).dirpath() + except KeyError: + return None + modpath = py.path.local(mod.__file__).dirpath() values: List[py.path.local] = [] - for relroot in relroots: - if not isinstance(relroot, py.path.local): + for relroot in relroots: + if not isinstance(relroot, py.path.local): relroot = relroot.replace("/", os.sep) - relroot = modpath.join(relroot, abs=True) - values.append(relroot) - return values - + relroot = modpath.join(relroot, abs=True) + values.append(relroot) + return values + def _get_override_ini_value(self, name: str) -> Optional[str]: - value = None + value = None # override_ini is a list of "ini=value" options. # 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. - for ini_config in self._override_ini: - try: - key, user_ini_value = ini_config.split("=", 1) + for ini_config in self._override_ini: + try: + key, user_ini_value = ini_config.split("=", 1) except ValueError as e: raise UsageError( "-o/--override-ini expects option=value style (got: {!r}).".format( ini_config ) ) from e - else: - if key == name: - value = user_ini_value - return value - + else: + if key == name: + value = user_ini_value + return value + def getoption(self, name: str, default=notset, skip: bool = False): """Return command line option value. - + :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. :param default: Default value if no option of that name exists. :param skip: If True, raise pytest.skip if option does not exists - or has a None value. - """ - name = self._opt2dest.get(name, name) - try: - val = getattr(self.option, name) - if val is None and skip: - raise AttributeError(name) - return val + or has a None value. + """ + name = self._opt2dest.get(name, name) + try: + val = getattr(self.option, name) + if val is None and skip: + raise AttributeError(name) + return val except AttributeError as e: - if default is not notset: - return default - if skip: - import pytest - + if default is not notset: + return default + if skip: + import pytest + pytest.skip(f"no {name!r} option found") raise ValueError(f"no option named {name!r}") from e - + def getvalue(self, name: str, path=None): """Deprecated, use getoption() instead.""" - return self.getoption(name) - + return self.getoption(name) + def getvalueorskip(self, name: str, path=None): """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: if not _assertion_supported(): if mode == "plain": @@ -1500,7 +1500,7 @@ class Config: self.issue_config_time_warning( PytestConfigWarning(warning_text), stacklevel=3, ) - + def _warn_about_skipped_plugins(self) -> None: for module_name, msg in self.pluginmanager.skipped_plugins: self.issue_config_time_warning( @@ -1510,53 +1510,53 @@ class Config: def _assertion_supported() -> bool: - try: - assert False - except AssertionError: - return True - else: + try: + assert False + except AssertionError: + return True + else: return False # type: ignore[unreachable] - - + + def create_terminal_writer( config: Config, file: Optional[TextIO] = None ) -> TerminalWriter: """Create a TerminalWriter instance configured according to the options in the config object. - + Every code which requires a TerminalWriter object and has access to a config object should use this function. """ tw = TerminalWriter(file=file) - - if config.option.color == "yes": - tw.hasmarkup = True + + if config.option.color == "yes": + tw.hasmarkup = True elif config.option.color == "no": - tw.hasmarkup = False + tw.hasmarkup = False if config.option.code_highlight == "yes": tw.code_highlight = True elif config.option.code_highlight == "no": tw.code_highlight = False - return tw - - + return tw + + def _strtobool(val: str) -> bool: """Convert a string representation of truth to True or False. - - True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values - are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if - 'val' is anything else. - + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + .. note:: Copied from distutils.util. - """ - val = val.lower() - if val in ("y", "yes", "t", "true", "on", "1"): + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): return True - elif val in ("n", "no", "f", "false", "off", "0"): + elif val in ("n", "no", "f", "false", "off", "0"): return False - else: + else: raise ValueError(f"invalid truth value {val!r}") diff --git a/contrib/python/pytest/py3/_pytest/config/argparsing.py b/contrib/python/pytest/py3/_pytest/config/argparsing.py index 20414f2b6e..9a48196552 100644 --- a/contrib/python/pytest/py3/_pytest/config/argparsing.py +++ b/contrib/python/pytest/py3/_pytest/config/argparsing.py @@ -1,6 +1,6 @@ -import argparse +import argparse import sys -import warnings +import warnings from gettext import gettext from typing import Any from typing import Callable @@ -13,28 +13,28 @@ from typing import Sequence from typing import Tuple from typing import TYPE_CHECKING from typing import Union - -import py - + +import py + import _pytest._io from _pytest.compat import final from _pytest.config.exceptions import UsageError - + if TYPE_CHECKING: from typing import NoReturn from typing_extensions import Literal -FILE_OR_DIR = "file_or_dir" - - +FILE_OR_DIR = "file_or_dir" + + @final class Parser: """Parser for command line arguments and ini-file values. - + :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. + """ + prog: Optional[str] = None def __init__( @@ -42,109 +42,109 @@ class Parser: usage: Optional[str] = None, processopt: Optional[Callable[["Argument"], None]] = None, ) -> None: - self._anonymous = OptionGroup("custom options", parser=self) + self._anonymous = OptionGroup("custom options", parser=self) self._groups: List[OptionGroup] = [] - self._processopt = processopt - self._usage = usage + self._processopt = processopt + self._usage = usage self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {} self._ininames: List[str] = [] self.extra_info: Dict[str, Any] = {} - + def processoption(self, option: "Argument") -> None: - if self._processopt: - if option.dest: - self._processopt(option) - + if self._processopt: + if option.dest: + self._processopt(option) + def getgroup( self, name: str, description: str = "", after: Optional[str] = None ) -> "OptionGroup": """Get (or create) a named option Group. - + :name: Name of the option group. :description: Long description for --help output. :after: Name of another group, used for ordering --help output. - - The returned group object has an ``addoption`` method with the same - signature as :py:func:`parser.addoption + + The returned group object has an ``addoption`` method with the same + signature as :py:func:`parser.addoption <_pytest.config.argparsing.Parser.addoption>` but will be shown in the - respective group in the output of ``pytest. --help``. - """ - for group in self._groups: - if group.name == name: - return group - group = OptionGroup(name, description, parser=self) - i = 0 - for i, grp in enumerate(self._groups): - if grp.name == after: - break - self._groups.insert(i + 1, group) - return group - + respective group in the output of ``pytest. --help``. + """ + for group in self._groups: + if group.name == name: + return group + group = OptionGroup(name, description, parser=self) + i = 0 + for i, grp in enumerate(self._groups): + if grp.name == after: + break + self._groups.insert(i + 1, group) + return group + def addoption(self, *opts: str, **attrs: Any) -> None: """Register a command line option. - + :opts: Option names, can be short or long options. :attrs: Same attributes which the ``add_argument()`` function of the `argparse library <https://docs.python.org/library/argparse.html>`_ - accepts. - + accepts. + After command line parsing, options are available on the pytest config - object via ``config.option.NAME`` where ``NAME`` is usually set - by passing a ``dest`` attribute, for example - ``addoption("--long", dest="NAME", ...)``. - """ - self._anonymous.addoption(*opts, **attrs) - + object via ``config.option.NAME`` where ``NAME`` is usually set + by passing a ``dest`` attribute, for example + ``addoption("--long", dest="NAME", ...)``. + """ + self._anonymous.addoption(*opts, **attrs) + def parse( self, args: Sequence[Union[str, py.path.local]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: - from _pytest._argcomplete import try_argcomplete - - self.optparser = self._getparser() - try_argcomplete(self.optparser) + from _pytest._argcomplete import try_argcomplete + + self.optparser = self._getparser() + try_argcomplete(self.optparser) strargs = [str(x) if isinstance(x, py.path.local) else x for x in args] return self.optparser.parse_args(strargs, namespace=namespace) - + def _getparser(self) -> "MyOptionParser": - from _pytest._argcomplete import filescompleter - + from _pytest._argcomplete import filescompleter + optparser = MyOptionParser(self, self.extra_info, prog=self.prog) - groups = self._groups + [self._anonymous] - for group in groups: - if group.options: - desc = group.description or group.name - arggroup = optparser.add_argument_group(desc) - for option in group.options: - n = option.names() - a = option.attrs() - arggroup.add_argument(*n, **a) + groups = self._groups + [self._anonymous] + for group in groups: + if group.options: + desc = group.description or group.name + arggroup = optparser.add_argument_group(desc) + for option in group.options: + n = option.names() + a = option.attrs() + arggroup.add_argument(*n, **a) file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*") - # bash like autocompletion for dirs (appending '/') + # bash like autocompletion for dirs (appending '/') # Type ignored because typeshed doesn't know about argcomplete. file_or_dir_arg.completer = filescompleter # type: ignore - return optparser - + return optparser + def parse_setoption( self, args: Sequence[Union[str, py.path.local]], option: argparse.Namespace, namespace: Optional[argparse.Namespace] = None, ) -> List[str]: - parsedoption = self.parse(args, namespace=namespace) - for name, value in parsedoption.__dict__.items(): - setattr(option, name, value) + parsedoption = self.parse(args, namespace=namespace) + for name, value in parsedoption.__dict__.items(): + setattr(option, name, value) return cast(List[str], getattr(parsedoption, FILE_OR_DIR)) - + def parse_known_args( self, args: Sequence[Union[str, py.path.local]], namespace: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: """Parse and return a namespace object with known arguments at this 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( self, args: Sequence[Union[str, py.path.local]], @@ -152,10 +152,10 @@ class Parser: ) -> Tuple[argparse.Namespace, List[str]]: """Parse and return a namespace object with known arguments, and 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] return optparser.parse_known_args(strargs, namespace=namespace) - + def addini( self, name: str, @@ -166,97 +166,97 @@ class Parser: default=None, ) -> None: """Register an ini-file option. - + :name: Name of the ini-variable. :type: Type of the variable, can be ``string``, ``pathlist``, ``args``, ``linelist`` or ``bool``. Defaults to ``string`` if ``None`` or not passed. :default: Default value if no ini-file option exists but is queried. - - The value of ini-variables can be retrieved via a call to - :py:func:`config.getini(name) <_pytest.config.Config.getini>`. - """ + + The value of ini-variables can be retrieved via a call to + :py:func:`config.getini(name) <_pytest.config.Config.getini>`. + """ assert type in (None, "string", "pathlist", "args", "linelist", "bool") - self._inidict[name] = (help, type, default) - self._ininames.append(name) - - -class ArgumentError(Exception): + self._inidict[name] = (help, type, default) + self._ininames.append(name) + + +class ArgumentError(Exception): """Raised if an Argument instance is created with invalid or inconsistent arguments.""" - + def __init__(self, msg: str, option: Union["Argument", str]) -> None: - self.msg = msg - self.option_id = str(option) - + self.msg = msg + self.option_id = str(option) + def __str__(self) -> str: - if self.option_id: + if self.option_id: return f"option {self.option_id}: {self.msg}" - else: - return self.msg - - + else: + return self.msg + + class Argument: """Class that mimics the necessary behaviour of optparse.Option. - + 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 - """ - - _typ_map = {"int": int, "string": str, "float": float, "complex": complex} - + https://docs.python.org/3/library/optparse.html#optparse-standard-option-types + """ + + _typ_map = {"int": int, "string": str, "float": float, "complex": complex} + def __init__(self, *names: str, **attrs: Any) -> None: """Store parms in private vars for use in add_argument.""" - self._attrs = attrs + self._attrs = attrs self._short_opts: List[str] = [] self._long_opts: List[str] = [] - if "%default" in (attrs.get("help") or ""): - warnings.warn( - 'pytest now uses argparse. "%default" should be' - ' changed to "%(default)s" ', - DeprecationWarning, - stacklevel=3, - ) - try: - typ = attrs["type"] - except KeyError: - pass - else: + if "%default" in (attrs.get("help") or ""): + warnings.warn( + 'pytest now uses argparse. "%default" should be' + ' changed to "%(default)s" ', + DeprecationWarning, + stacklevel=3, + ) + try: + typ = attrs["type"] + except KeyError: + pass + else: # This might raise a keyerror as well, don't want to catch that. if isinstance(typ, str): - if typ == "choice": - warnings.warn( - "`type` argument to addoption() is the string %r." - " For choices this is optional and can be omitted, " - " but when supplied should be a type (for example `str` or `int`)." - " (options: %s)" % (typ, names), - DeprecationWarning, - stacklevel=4, - ) - # argparse expects a type here take it from - # the type of the first element - attrs["type"] = type(attrs["choices"][0]) - else: - warnings.warn( - "`type` argument to addoption() is the string %r, " - " but when supplied should be a type (for example `str` or `int`)." - " (options: %s)" % (typ, names), - DeprecationWarning, - stacklevel=4, - ) - attrs["type"] = Argument._typ_map[typ] + if typ == "choice": + warnings.warn( + "`type` argument to addoption() is the string %r." + " For choices this is optional and can be omitted, " + " but when supplied should be a type (for example `str` or `int`)." + " (options: %s)" % (typ, names), + DeprecationWarning, + stacklevel=4, + ) + # argparse expects a type here take it from + # the type of the first element + attrs["type"] = type(attrs["choices"][0]) + else: + warnings.warn( + "`type` argument to addoption() is the string %r, " + " but when supplied should be a type (for example `str` or `int`)." + " (options: %s)" % (typ, names), + DeprecationWarning, + stacklevel=4, + ) + attrs["type"] = Argument._typ_map[typ] # Used in test_parseopt -> test_parse_defaultgetter. - self.type = attrs["type"] - else: - self.type = typ - try: + self.type = attrs["type"] + else: + self.type = typ + try: # Attribute existence is tested in Config._processopt. - self.default = attrs["default"] - except KeyError: - pass - self._set_opt_strings(names) + self.default = attrs["default"] + except KeyError: + pass + self._set_opt_strings(names) dest: Optional[str] = attrs.get("dest") if dest: self.dest = dest @@ -268,136 +268,136 @@ class Argument: except IndexError as e: self.dest = "???" # Needed for the error repr. raise ArgumentError("need a long or short option", self) from e - + def names(self) -> List[str]: - return self._short_opts + self._long_opts - + return self._short_opts + self._long_opts + def attrs(self) -> Mapping[str, Any]: # Update any attributes set by processopt. - attrs = "default dest help".split() + attrs = "default dest help".split() attrs.append(self.dest) - for attr in attrs: - try: - self._attrs[attr] = getattr(self, attr) - except AttributeError: - pass - if self._attrs.get("help"): - a = self._attrs["help"] - a = a.replace("%default", "%(default)s") - # a = a.replace('%prog', '%(prog)s') - self._attrs["help"] = a - return self._attrs - + for attr in attrs: + try: + self._attrs[attr] = getattr(self, attr) + except AttributeError: + pass + if self._attrs.get("help"): + a = self._attrs["help"] + a = a.replace("%default", "%(default)s") + # a = a.replace('%prog', '%(prog)s') + self._attrs["help"] = a + return self._attrs + def _set_opt_strings(self, opts: Sequence[str]) -> None: """Directly from optparse. - + Might not be necessary as this is passed to argparse later on. """ - for opt in opts: - if len(opt) < 2: - raise ArgumentError( - "invalid option string %r: " - "must be at least two characters long" % opt, - self, - ) - elif len(opt) == 2: - if not (opt[0] == "-" and opt[1] != "-"): - raise ArgumentError( - "invalid short option string %r: " - "must be of the form -x, (x any non-dash char)" % opt, - self, - ) - self._short_opts.append(opt) - else: - if not (opt[0:2] == "--" and opt[2] != "-"): - raise ArgumentError( - "invalid long option string %r: " - "must start with --, followed by non-dash" % opt, - self, - ) - self._long_opts.append(opt) - + for opt in opts: + if len(opt) < 2: + raise ArgumentError( + "invalid option string %r: " + "must be at least two characters long" % opt, + self, + ) + elif len(opt) == 2: + if not (opt[0] == "-" and opt[1] != "-"): + raise ArgumentError( + "invalid short option string %r: " + "must be of the form -x, (x any non-dash char)" % opt, + self, + ) + self._short_opts.append(opt) + else: + if not (opt[0:2] == "--" and opt[2] != "-"): + raise ArgumentError( + "invalid long option string %r: " + "must start with --, followed by non-dash" % opt, + self, + ) + self._long_opts.append(opt) + def __repr__(self) -> str: args: List[str] = [] - if self._short_opts: - args += ["_short_opts: " + repr(self._short_opts)] - if self._long_opts: - args += ["_long_opts: " + repr(self._long_opts)] - args += ["dest: " + repr(self.dest)] - if hasattr(self, "type"): - args += ["type: " + repr(self.type)] - if hasattr(self, "default"): - args += ["default: " + repr(self.default)] - return "Argument({})".format(", ".join(args)) - - + if self._short_opts: + args += ["_short_opts: " + repr(self._short_opts)] + if self._long_opts: + args += ["_long_opts: " + repr(self._long_opts)] + args += ["dest: " + repr(self.dest)] + if hasattr(self, "type"): + args += ["type: " + repr(self.type)] + if hasattr(self, "default"): + args += ["default: " + repr(self.default)] + return "Argument({})".format(", ".join(args)) + + class OptionGroup: def __init__( self, name: str, description: str = "", parser: Optional[Parser] = None ) -> None: - self.name = name - self.description = description + self.name = name + self.description = description self.options: List[Argument] = [] - self.parser = parser - + self.parser = parser + def addoption(self, *optnames: str, **attrs: Any) -> None: """Add an option to this group. - + If a shortened version of a long option is specified, it will - be suppressed in the help. addoption('--twowords', '--two-words') - results in help showing '--two-words' only, but --twowords gets + be suppressed in the help. addoption('--twowords', '--two-words') + results in help showing '--two-words' only, but --twowords gets accepted **and** the automatic destination is in args.twowords. - """ - conflict = set(optnames).intersection( - name for opt in self.options for name in opt.names() - ) - if conflict: - raise ValueError("option names %s already added" % conflict) - option = Argument(*optnames, **attrs) - self._addoption_instance(option, shortupper=False) - + """ + conflict = set(optnames).intersection( + name for opt in self.options for name in opt.names() + ) + if conflict: + raise ValueError("option names %s already added" % conflict) + option = Argument(*optnames, **attrs) + self._addoption_instance(option, shortupper=False) + def _addoption(self, *optnames: str, **attrs: Any) -> None: - option = Argument(*optnames, **attrs) - self._addoption_instance(option, shortupper=True) - + option = Argument(*optnames, **attrs) + self._addoption_instance(option, shortupper=True) + def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: - if not shortupper: - for opt in option._short_opts: - if opt[0] == "-" and opt[1].islower(): - raise ValueError("lowercase shortoptions reserved") - if self.parser: - self.parser.processoption(option) - self.options.append(option) - - -class MyOptionParser(argparse.ArgumentParser): + if not shortupper: + for opt in option._short_opts: + if opt[0] == "-" and opt[1].islower(): + raise ValueError("lowercase shortoptions reserved") + if self.parser: + self.parser.processoption(option) + self.options.append(option) + + +class MyOptionParser(argparse.ArgumentParser): def __init__( self, parser: Parser, extra_info: Optional[Dict[str, Any]] = None, prog: Optional[str] = None, ) -> None: - self._parser = parser - argparse.ArgumentParser.__init__( - self, + self._parser = parser + argparse.ArgumentParser.__init__( + self, prog=prog, - usage=parser._usage, - add_help=False, - formatter_class=DropShorterLongHelpFormatter, + usage=parser._usage, + add_help=False, + formatter_class=DropShorterLongHelpFormatter, 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. self.extra_info = extra_info if extra_info else {} - + def error(self, message: str) -> "NoReturn": """Transform argparse error message into UsageError.""" msg = f"{self.prog}: error: {message}" - + if hasattr(self._parser, "_config_source_hint"): # Type ignored because the attribute is set dynamically. msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore - + raise UsageError(self.format_usage() + msg) # Type ignored because typeshed has a very complex type in the superclass. @@ -410,14 +410,14 @@ class MyOptionParser(argparse.ArgumentParser): parsed, unrecognized = self.parse_known_args(args, namespace) if unrecognized: for arg in unrecognized: - if arg and arg[0] == "-": + if arg and arg[0] == "-": lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] - for k, v in sorted(self.extra_info.items()): + for k, v in sorted(self.extra_info.items()): lines.append(f" {k}: {v}") - self.error("\n".join(lines)) + self.error("\n".join(lines)) getattr(parsed, FILE_OR_DIR).extend(unrecognized) return parsed - + if sys.version_info[:2] < (3, 9): # pragma: no cover # Backport of https://github.com/python/cpython/pull/14316 so we can # disable long --argument abbreviations without breaking short flags. @@ -455,16 +455,16 @@ class MyOptionParser(argparse.ArgumentParser): if " " in arg_string: return None return None, arg_string, None - -class DropShorterLongHelpFormatter(argparse.HelpFormatter): + +class DropShorterLongHelpFormatter(argparse.HelpFormatter): """Shorten help for long options that differ only in 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. - Cache result on the action object as this is called at least 2 times. - """ - + """ + def __init__(self, *args: Any, **kwargs: Any) -> None: # Use more accurate terminal width. if "width" not in kwargs: @@ -472,39 +472,39 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): super().__init__(*args, **kwargs) def _format_action_invocation(self, action: argparse.Action) -> str: - orgstr = argparse.HelpFormatter._format_action_invocation(self, action) - if orgstr and orgstr[0] != "-": # only optional arguments - return orgstr + orgstr = argparse.HelpFormatter._format_action_invocation(self, action) + if orgstr and orgstr[0] != "-": # only optional arguments + return orgstr res: Optional[str] = getattr(action, "_formatted_action_invocation", None) - if res: - return res - options = orgstr.split(", ") - if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): - # a shortcut for '-h, --help' or '--abc', '-a' + if res: + return res + options = orgstr.split(", ") + if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): + # a shortcut for '-h, --help' or '--abc', '-a' action._formatted_action_invocation = orgstr # type: ignore - return orgstr - return_list = [] + return orgstr + return_list = [] short_long: Dict[str, str] = {} - for option in options: - if len(option) == 2 or option[2] == " ": - continue - if not option.startswith("--"): - raise ArgumentError( + for option in options: + if len(option) == 2 or option[2] == " ": + continue + if not option.startswith("--"): + raise ArgumentError( 'long optional argument without "--": [%s]' % (option), option - ) - xxoption = option[2:] + ) + xxoption = option[2:] shortened = xxoption.replace("-", "") if shortened not in short_long or len(short_long[shortened]) < len( xxoption ): short_long[shortened] = xxoption - # now short_long has been filled out to the longest with dashes - # **and** we keep the right option ordering from add_argument - for option in options: - if len(option) == 2 or option[2] == " ": - return_list.append(option) - if option[2:] == short_long.get(option.replace("-", "")): - return_list.append(option.replace(" ", "=", 1)) + # now short_long has been filled out to the longest with dashes + # **and** we keep the right option ordering from add_argument + for option in options: + if len(option) == 2 or option[2] == " ": + return_list.append(option) + if option[2:] == short_long.get(option.replace("-", "")): + return_list.append(option.replace(" ", "=", 1)) formatted_action_invocation = ", ".join(return_list) action._formatted_action_invocation = formatted_action_invocation # type: ignore return formatted_action_invocation diff --git a/contrib/python/pytest/py3/_pytest/config/exceptions.py b/contrib/python/pytest/py3/_pytest/config/exceptions.py index a6441629b2..4f1320e758 100644 --- a/contrib/python/pytest/py3/_pytest/config/exceptions.py +++ b/contrib/python/pytest/py3/_pytest/config/exceptions.py @@ -2,10 +2,10 @@ from _pytest.compat import final @final -class UsageError(Exception): +class UsageError(Exception): """Error in pytest usage or invocation.""" - - -class PrintHelp(Exception): + + +class PrintHelp(Exception): """Raised when pytest should print its help to skip the rest of the - argument parsing and validation.""" + argument parsing and validation.""" diff --git a/contrib/python/pytest/py3/_pytest/config/findpaths.py b/contrib/python/pytest/py3/_pytest/config/findpaths.py index 7eaa4e5c35..2edf54536b 100644 --- a/contrib/python/pytest/py3/_pytest/config/findpaths.py +++ b/contrib/python/pytest/py3/_pytest/config/findpaths.py @@ -1,4 +1,4 @@ -import os +import os from pathlib import Path from typing import Dict from typing import Iterable @@ -8,17 +8,17 @@ from typing import Sequence from typing import Tuple from typing import TYPE_CHECKING from typing import Union - + import iniconfig - -from .exceptions import UsageError + +from .exceptions import UsageError from _pytest.outcomes import fail from _pytest.pathlib import absolutepath from _pytest.pathlib import commonpath - + if TYPE_CHECKING: from . import Config - + def _parse_ini_config(path: Path) -> iniconfig.IniConfig: """Parse the given generic '.ini' file using legacy IniConfig parser, returning @@ -26,20 +26,20 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig: Raise UsageError if the file cannot be parsed. """ - try: + try: return iniconfig.IniConfig(str(path)) except iniconfig.ParseError as exc: raise UsageError(str(exc)) from exc - - + + def load_config_dict_from_file( filepath: Path, ) -> Optional[Dict[str, Union[str, List[str]]]]: """Load pytest configuration from the given file path, if supported. Return None if the file does not contain valid pytest configuration. - """ - + """ + # Configuration from ini files are obtained from the [pytest] section, if present. if filepath.suffix == ".ini": iniconfig = _parse_ini_config(filepath) @@ -94,10 +94,10 @@ def locate_config( "tox.ini", "setup.cfg", ] - args = [x for x in args if not str(x).startswith("-")] - if not args: + args = [x for x in args if not str(x).startswith("-")] + if not args: args = [Path.cwd()] - for arg in args: + for arg in args: argpath = absolutepath(arg) for base in (argpath, *argpath.parents): for config_name in config_names: @@ -107,43 +107,43 @@ def locate_config( if ini_config is not None: return base, p, ini_config return None, None, {} - + def get_common_ancestor(paths: Iterable[Path]) -> Path: common_ancestor: Optional[Path] = None - for path in paths: - if not path.exists(): - continue - if common_ancestor is None: - common_ancestor = path - else: + for path in paths: + if not path.exists(): + continue + if common_ancestor is None: + common_ancestor = path + else: if common_ancestor in path.parents or path == common_ancestor: - continue + continue elif path in common_ancestor.parents: - common_ancestor = path - else: + common_ancestor = path + else: shared = commonpath(path, common_ancestor) - if shared is not None: - common_ancestor = shared - if common_ancestor is None: + if shared is not None: + common_ancestor = shared + if common_ancestor is None: common_ancestor = Path.cwd() elif common_ancestor.is_file(): common_ancestor = common_ancestor.parent - return common_ancestor - - + return common_ancestor + + def get_dirs_from_args(args: Iterable[str]) -> List[Path]: def is_option(x: str) -> bool: return x.startswith("-") - + def get_file_part_from_node_id(x: str) -> str: return x.split("::")[0] - + def get_dir_from_path(path: Path) -> Path: if path.is_dir(): - return path + return path return path.parent - + def safe_exists(path: Path) -> bool: # This can throw on paths that contain characters unrepresentable at the OS level, # or with invalid syntax on Windows (https://bugs.python.org/issue35306) @@ -152,16 +152,16 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]: except OSError: return False - # These look like paths but may not exist - possible_paths = ( + # These look like paths but may not exist + possible_paths = ( absolutepath(get_file_part_from_node_id(arg)) - for arg in args - if not is_option(arg) - ) - + for arg in args + if not is_option(arg) + ) + return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)] - - + + CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." @@ -172,40 +172,40 @@ def determine_setup( config: Optional["Config"] = None, ) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: rootdir = None - dirs = get_dirs_from_args(args) - if inifile: + dirs = get_dirs_from_args(args) + if inifile: inipath_ = absolutepath(inifile) inipath: Optional[Path] = inipath_ inicfg = load_config_dict_from_file(inipath_) or {} if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) - else: - ancestor = get_common_ancestor(dirs) + else: + ancestor = get_common_ancestor(dirs) rootdir, inipath, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in (ancestor, *ancestor.parents): if (possible_rootdir / "setup.py").is_file(): rootdir = possible_rootdir - break - else: + break + else: if dirs != [ancestor]: rootdir, inipath, inicfg = locate_config(dirs) - if rootdir is None: + if rootdir is None: if config is not None: cwd = config.invocation_params.dir else: cwd = Path.cwd() rootdir = get_common_ancestor([cwd, ancestor]) - is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" - if is_fs_root: - rootdir = ancestor - if rootdir_cmd_arg: + is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" + if is_fs_root: + rootdir = ancestor + if rootdir_cmd_arg: rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) if not rootdir.is_dir(): - raise UsageError( - "Directory '{}' not found. Check your '--rootdir' option.".format( + raise UsageError( + "Directory '{}' not found. Check your '--rootdir' option.".format( rootdir - ) - ) + ) + ) assert rootdir is not None return rootdir, inipath, inicfg or {} diff --git a/contrib/python/pytest/py3/_pytest/debugging.py b/contrib/python/pytest/py3/_pytest/debugging.py index 6aacbd940c..b52840006b 100644 --- a/contrib/python/pytest/py3/_pytest/debugging.py +++ b/contrib/python/pytest/py3/_pytest/debugging.py @@ -1,8 +1,8 @@ """Interactive debugging with PDB, the Python Debugger.""" import argparse import functools -import os -import sys +import os +import sys import types from typing import Any from typing import Callable @@ -13,59 +13,59 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union - -from _pytest import outcomes + +from _pytest import outcomes from _pytest._code import ExceptionInfo from _pytest.config import Config from _pytest.config import ConftestImportFailure -from _pytest.config import hookimpl +from _pytest.config import hookimpl from _pytest.config import PytestPluginManager from _pytest.config.argparsing import Parser from _pytest.config.exceptions import UsageError from _pytest.nodes import Node from _pytest.reports import BaseReport - + if TYPE_CHECKING: from _pytest.capture import CaptureManager from _pytest.runner import CallInfo - - -def import_readline(): - try: - import readline - except ImportError: - sys.path.append('/usr/lib/python2.7/lib-dynload') - - try: - import readline - except ImportError as e: - print('can not import readline:', e) - - import subprocess - try: - subprocess.check_call('stty icrnl'.split()) - except OSError as e: - print('can not restore Enter, use Control+J:', e) - - -def tty(): - if os.isatty(1): - return - + + +def import_readline(): + try: + import readline + except ImportError: + sys.path.append('/usr/lib/python2.7/lib-dynload') + + try: + import readline + except ImportError as e: + print('can not import readline:', e) + + import subprocess + try: + subprocess.check_call('stty icrnl'.split()) + except OSError as e: + print('can not restore Enter, use Control+J:', e) + + +def tty(): + if os.isatty(1): + return + fd = os.open('/dev/tty', os.O_RDWR) - os.dup2(fd, 0) - os.dup2(fd, 1) - os.dup2(fd, 2) + os.dup2(fd, 0) + os.dup2(fd, 1) + os.dup2(fd, 2) os.close(fd) - - old_sys_path = sys.path - sys.path = list(sys.path) - try: - import_readline() - finally: - sys.path = old_sys_path - - + + old_sys_path = sys.path + sys.path = list(sys.path) + try: + import_readline() + finally: + sys.path = old_sys_path + + def _validate_usepdb_cls(value: str) -> Tuple[str, str]: """Validate syntax of --pdbcls option.""" try: @@ -78,59 +78,59 @@ def _validate_usepdb_cls(value: str) -> Tuple[str, str]: def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group._addoption( - "--pdb", - dest="usepdb", - action="store_true", - help="start the interactive Python debugger on errors or KeyboardInterrupt.", - ) - group._addoption( - "--pdbcls", - dest="usepdb_cls", - metavar="modulename:classname", + group = parser.getgroup("general") + group._addoption( + "--pdb", + dest="usepdb", + action="store_true", + help="start the interactive Python debugger on errors or KeyboardInterrupt.", + ) + group._addoption( + "--pdbcls", + dest="usepdb_cls", + metavar="modulename:classname", type=_validate_usepdb_cls, - help="start a custom interactive Python debugger on errors. " - "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", - ) - group._addoption( - "--trace", - dest="trace", - action="store_true", - help="Immediately break when running each test.", - ) - - + help="start a custom interactive Python debugger on errors. " + "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb", + ) + group._addoption( + "--trace", + dest="trace", + action="store_true", + help="Immediately break when running each test.", + ) + + def pytest_configure(config: Config) -> None: import pdb - if config.getvalue("trace"): - config.pluginmanager.register(PdbTrace(), "pdbtrace") - if config.getvalue("usepdb"): - config.pluginmanager.register(PdbInvoke(), "pdbinvoke") - - pytestPDB._saved.append( + if config.getvalue("trace"): + config.pluginmanager.register(PdbTrace(), "pdbtrace") + if config.getvalue("usepdb"): + config.pluginmanager.register(PdbInvoke(), "pdbinvoke") + + pytestPDB._saved.append( (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config) - ) - pdb.set_trace = pytestPDB.set_trace - pytestPDB._pluginmanager = config.pluginmanager - pytestPDB._config = config - - # NOTE: not using pytest_unconfigure, since it might get called although - # pytest_configure was not (if another plugin raises UsageError). + ) + pdb.set_trace = pytestPDB.set_trace + pytestPDB._pluginmanager = config.pluginmanager + pytestPDB._config = config + + # NOTE: not using pytest_unconfigure, since it might get called although + # pytest_configure was not (if another plugin raises UsageError). def fin() -> None: - ( - pdb.set_trace, - pytestPDB._pluginmanager, - pytestPDB._config, - ) = pytestPDB._saved.pop() - - config._cleanup.append(fin) - - + ( + pdb.set_trace, + pytestPDB._pluginmanager, + pytestPDB._config, + ) = pytestPDB._saved.pop() + + config._cleanup.append(fin) + + class pytestPDB: """Pseudo PDB that defers to the real pdb.""" - + _pluginmanager: Optional[PytestPluginManager] = None _config: Optional[Config] = None _saved: List[ @@ -138,8 +138,8 @@ class pytestPDB: ] = [] _recursive_debug = 0 _wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None - - @classmethod + + @classmethod def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]: if capman: return capman.is_capturing() @@ -186,20 +186,20 @@ class pytestPDB: @classmethod def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]): - import _pytest.config - + import _pytest.config + # Type ignored because mypy doesn't support "dynamic" # inheritance like this. class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc] _pytest_capman = capman _continued = False - + def do_debug(self, arg): cls._recursive_debug += 1 ret = super().do_debug(arg) cls._recursive_debug -= 1 return ret - + def do_continue(self, arg): ret = super().do_continue(arg) if cls._recursive_debug == 0: @@ -211,8 +211,8 @@ class pytestPDB: capturing = pytestPDB._is_capturing(capman) if capturing: if capturing == "global": - tw.sep(">", "PDB continue (IO-capturing resumed)") - else: + tw.sep(">", "PDB continue (IO-capturing resumed)") + else: tw.sep( ">", "PDB continue (IO-capturing resumed for %s)" @@ -226,18 +226,18 @@ class pytestPDB: cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self) self._continued = True return ret - + do_c = do_cont = do_continue - + def do_quit(self, arg): """Raise Exit outcome when quit command is used in pdb. - + This is a bit of a hack - it would be better if BdbQuit could be handled, but this would require to wrap the whole pytest run, and adjust the report etc. """ ret = super().do_quit(arg) - + if cls._recursive_debug == 0: outcomes.exit("Quitting debugger") @@ -282,11 +282,11 @@ class pytestPDB: capman = cls._pluginmanager.getplugin("capturemanager") if capman: capman.suspend(in_=True) - + if cls._config: tw = _pytest.config.create_terminal_writer(cls._config) tw.line() - + if cls._recursive_debug == 0: # Handle header similar to pdb.set_trace in py37+. header = kwargs.pop("header", None) @@ -304,7 +304,7 @@ class pytestPDB: ) else: tw.sep(">", f"PDB {method}") - + _pdb = cls._import_pdb_cls(capman)(**kwargs) if cls._pluginmanager: @@ -324,36 +324,36 @@ class PdbInvoke: def pytest_exception_interact( self, node: Node, call: "CallInfo[Any]", report: BaseReport ) -> None: - capman = node.config.pluginmanager.getplugin("capturemanager") - if capman: - capman.suspend_global_capture(in_=True) - out, err = capman.read_global_capture() - sys.stdout.write(out) - sys.stdout.write(err) - tty() + capman = node.config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture(in_=True) + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stdout.write(err) + tty() assert call.excinfo is not None - _enter_pdb(node, call.excinfo, report) - + _enter_pdb(node, call.excinfo, report) + def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: - tb = _postmortem_traceback(excinfo) - post_mortem(tb) - - + tb = _postmortem_traceback(excinfo) + post_mortem(tb) + + class PdbTrace: - @hookimpl(hookwrapper=True) + @hookimpl(hookwrapper=True) def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: wrap_pytest_function_for_tracing(pyfuncitem) - yield - - + yield + + def wrap_pytest_function_for_tracing(pyfuncitem): """Change the Python function object of the given Function item by a wrapper which actually enters pdb before calling the python function itself, effectively leaving the user in the pdb prompt in the first statement of the function.""" _pdb = pytestPDB._init_pdb("runcall") - testfunction = pyfuncitem.obj - + testfunction = pyfuncitem.obj + # 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 # an exception if one of the kwargs to testfunction was called `func`. @@ -361,7 +361,7 @@ def wrap_pytest_function_for_tracing(pyfuncitem): def wrapper(*args, **kwargs): func = functools.partial(testfunction, *args, **kwargs) _pdb.runcall(func) - + pyfuncitem.obj = wrapper @@ -375,53 +375,53 @@ def maybe_wrap_pytest_function_for_tracing(pyfuncitem): def _enter_pdb( node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport ) -> BaseReport: - # XXX we re-use the TerminalReporter's terminalwriter - # because this seems to avoid some encoding related troubles - # for not completely clear reasons. - tw = node.config.pluginmanager.getplugin("terminalreporter")._tw - tw.line() - - showcapture = node.config.option.showcapture - - for sectionname, content in ( - ("stdout", rep.capstdout), - ("stderr", rep.capstderr), - ("log", rep.caplog), - ): - if showcapture in (sectionname, "all") and content: - tw.sep(">", "captured " + sectionname) - if content[-1:] == "\n": - content = content[:-1] - tw.line(content) - - tw.sep(">", "traceback") - rep.toterminal(tw) - tw.sep(">", "entering PDB") - tb = _postmortem_traceback(excinfo) + # XXX we re-use the TerminalReporter's terminalwriter + # because this seems to avoid some encoding related troubles + # for not completely clear reasons. + tw = node.config.pluginmanager.getplugin("terminalreporter")._tw + tw.line() + + showcapture = node.config.option.showcapture + + for sectionname, content in ( + ("stdout", rep.capstdout), + ("stderr", rep.capstderr), + ("log", rep.caplog), + ): + if showcapture in (sectionname, "all") and content: + tw.sep(">", "captured " + sectionname) + if content[-1:] == "\n": + content = content[:-1] + tw.line(content) + + tw.sep(">", "traceback") + rep.toterminal(tw) + tw.sep(">", "entering PDB") + tb = _postmortem_traceback(excinfo) rep._pdbshown = True # type: ignore[attr-defined] post_mortem(tb) - return rep - - + return rep + + def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType: from doctest import UnexpectedException - if isinstance(excinfo.value, UnexpectedException): - # A doctest.UnexpectedException is not useful for post_mortem. - # Use the underlying exception instead: - return excinfo.value.exc_info[2] + if isinstance(excinfo.value, UnexpectedException): + # A doctest.UnexpectedException is not useful for post_mortem. + # Use the underlying exception instead: + return excinfo.value.exc_info[2] elif isinstance(excinfo.value, ConftestImportFailure): # A config.ConftestImportFailure is not useful for post_mortem. # Use the underlying exception instead: return excinfo.value.excinfo[2] - else: + else: assert excinfo._excinfo is not None - return excinfo._excinfo[2] - - + return excinfo._excinfo[2] + + def post_mortem(t: types.TracebackType) -> None: p = pytestPDB._init_pdb("post_mortem") - p.reset() - p.interaction(None, t) + p.reset() + p.interaction(None, t) if p.quitting: outcomes.exit("Quitting debugger") diff --git a/contrib/python/pytest/py3/_pytest/deprecated.py b/contrib/python/pytest/py3/_pytest/deprecated.py index 43debd1734..19b31d6653 100644 --- a/contrib/python/pytest/py3/_pytest/deprecated.py +++ b/contrib/python/pytest/py3/_pytest/deprecated.py @@ -1,18 +1,18 @@ """Deprecation messages and bits of code used elsewhere in the codebase 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 -be removed when the time comes. - + +Keeping it in a central location makes it easy to track what is deprecated and should +be removed when the time comes. + All constants defined in this module should be either instances of :class:`PytestWarning`, or :class:`UnformattedWarning` -in case of warnings which need to format their messages. -""" +in case of warnings which need to format their messages. +""" from warnings import warn -from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warning_types import UnformattedWarning - +from _pytest.warning_types import PytestDeprecationWarning +from _pytest.warning_types import UnformattedWarning + # set of plugins which have been integrated into the core; we use this list to ignore # them during registration to avoid conflicts DEPRECATED_EXTERNAL_PLUGINS = { @@ -20,48 +20,48 @@ DEPRECATED_EXTERNAL_PLUGINS = { "pytest_capturelog", "pytest_faulthandler", } - + FILLFUNCARGS = UnformattedWarning( PytestDeprecationWarning, "{name} is deprecated, use " "function._request._fillfixtures() instead if you cannot avoid reaching into internals.", -) - +) + PYTEST_COLLECT_MODULE = UnformattedWarning( PytestDeprecationWarning, "pytest.collect.{name} was moved to pytest.{name}\n" "Please update to the new name.", -) - +) + YIELD_FIXTURE = PytestDeprecationWarning( "@pytest.yield_fixture is deprecated.\n" "Use @pytest.fixture instead; they are the same." -) - +) + MINUS_K_DASH = PytestDeprecationWarning( "The `-k '-expr'` syntax to -k is deprecated.\nUse `-k 'not expr'` instead." -) - +) + MINUS_K_COLON = PytestDeprecationWarning( "The `-k 'expr:'` syntax to -k is deprecated.\n" "Please open an issue if you use this and want a replacement." -) - +) + WARNING_CAPTURED_HOOK = PytestDeprecationWarning( "The pytest_warning_captured is deprecated and will be removed in a future release.\n" "Please use pytest_warning_recorded instead." -) - +) + FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestDeprecationWarning( "The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; " "use self.session.gethookproxy() and self.session.isinitpath() instead. " -) - +) + STRICT_OPTION = PytestDeprecationWarning( "The --strict option is deprecated, use --strict-markers instead." ) - + PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") diff --git a/contrib/python/pytest/py3/_pytest/doctest.py b/contrib/python/pytest/py3/_pytest/doctest.py index 8e4b1ca6ad..64e8f0e0ee 100644 --- a/contrib/python/pytest/py3/_pytest/doctest.py +++ b/contrib/python/pytest/py3/_pytest/doctest.py @@ -1,9 +1,9 @@ """Discover and run doctests in modules and test files.""" import bdb import inspect -import platform -import sys -import traceback +import platform +import sys +import traceback import types import warnings from contextlib import contextmanager @@ -20,98 +20,98 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union - + import py.path -import pytest +import pytest from _pytest import outcomes -from _pytest._code.code import ExceptionInfo -from _pytest._code.code import ReprFileLocation -from _pytest._code.code import TerminalRepr +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ReprFileLocation +from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import safe_getattr from _pytest.config import Config from _pytest.config.argparsing import Parser -from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import FixtureRequest from _pytest.nodes import Collector from _pytest.outcomes import OutcomeException from _pytest.pathlib import import_path from _pytest.python_api import approx from _pytest.warning_types import PytestWarning - + if TYPE_CHECKING: import doctest -DOCTEST_REPORT_CHOICE_NONE = "none" -DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" -DOCTEST_REPORT_CHOICE_NDIFF = "ndiff" -DOCTEST_REPORT_CHOICE_UDIFF = "udiff" -DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure" - -DOCTEST_REPORT_CHOICES = ( - DOCTEST_REPORT_CHOICE_NONE, - DOCTEST_REPORT_CHOICE_CDIFF, - DOCTEST_REPORT_CHOICE_NDIFF, - DOCTEST_REPORT_CHOICE_UDIFF, - DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE, -) - -# Lazy definition of runner class -RUNNER_CLASS = None +DOCTEST_REPORT_CHOICE_NONE = "none" +DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" +DOCTEST_REPORT_CHOICE_NDIFF = "ndiff" +DOCTEST_REPORT_CHOICE_UDIFF = "udiff" +DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure" + +DOCTEST_REPORT_CHOICES = ( + DOCTEST_REPORT_CHOICE_NONE, + DOCTEST_REPORT_CHOICE_CDIFF, + DOCTEST_REPORT_CHOICE_NDIFF, + DOCTEST_REPORT_CHOICE_UDIFF, + DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE, +) + +# Lazy definition of runner class +RUNNER_CLASS = None # Lazy definition of output checker class CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None - - + + def pytest_addoption(parser: Parser) -> None: - parser.addini( - "doctest_optionflags", - "option flags for doctests", - type="args", - default=["ELLIPSIS"], - ) - parser.addini( - "doctest_encoding", "encoding used for doctest files", default="utf-8" - ) - group = parser.getgroup("collect") - group.addoption( - "--doctest-modules", - action="store_true", - default=False, - help="run doctests in all .py modules", - dest="doctestmodules", - ) - group.addoption( - "--doctest-report", - type=str.lower, - default="udiff", - help="choose another output format for diffs on doctest failure", - choices=DOCTEST_REPORT_CHOICES, - dest="doctestreport", - ) - group.addoption( - "--doctest-glob", - action="append", - default=[], - metavar="pat", - help="doctests file matching pattern, default: test*.txt", - dest="doctestglob", - ) - group.addoption( - "--doctest-ignore-import-errors", - action="store_true", - default=False, - help="ignore doctest ImportErrors", - dest="doctest_ignore_import_errors", - ) - group.addoption( - "--doctest-continue-on-failure", - action="store_true", - default=False, - help="for a given doctest, continue to run after the first failure", - dest="doctest_continue_on_failure", - ) - - + parser.addini( + "doctest_optionflags", + "option flags for doctests", + type="args", + default=["ELLIPSIS"], + ) + parser.addini( + "doctest_encoding", "encoding used for doctest files", default="utf-8" + ) + group = parser.getgroup("collect") + group.addoption( + "--doctest-modules", + action="store_true", + default=False, + help="run doctests in all .py modules", + dest="doctestmodules", + ) + group.addoption( + "--doctest-report", + type=str.lower, + default="udiff", + help="choose another output format for diffs on doctest failure", + choices=DOCTEST_REPORT_CHOICES, + dest="doctestreport", + ) + group.addoption( + "--doctest-glob", + action="append", + default=[], + metavar="pat", + help="doctests file matching pattern, default: test*.txt", + dest="doctestglob", + ) + group.addoption( + "--doctest-ignore-import-errors", + action="store_true", + default=False, + help="ignore doctest ImportErrors", + dest="doctest_ignore_import_errors", + ) + group.addoption( + "--doctest-continue-on-failure", + action="store_true", + default=False, + help="for a given doctest, continue to run after the first failure", + dest="doctest_continue_on_failure", + ) + + def pytest_unconfigure() -> None: global RUNNER_CLASS @@ -121,84 +121,84 @@ def pytest_unconfigure() -> None: def pytest_collect_file( path: py.path.local, parent: Collector, ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: - config = parent.config - if path.ext == ".py": + config = parent.config + if path.ext == ".py": if config.option.doctestmodules and not _is_setup_py(path): mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path) return mod - elif _is_doctest(config, path, parent): + elif _is_doctest(config, path, parent): txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path) return txt return None - - + + def _is_setup_py(path: py.path.local) -> bool: - if path.basename != "setup.py": - return False + if path.basename != "setup.py": + return False contents = path.read_binary() return b"setuptools" in contents or b"distutils" in contents - - + + def _is_doctest(config: Config, path: py.path.local, parent) -> bool: - if path.ext in (".txt", ".rst") and parent.session.isinitpath(path): - return True - globs = config.getoption("doctestglob") or ["test*.txt"] - for glob in globs: - if path.check(fnmatch=glob): - return True - return False - - -class ReprFailDoctest(TerminalRepr): + if path.ext in (".txt", ".rst") and parent.session.isinitpath(path): + return True + globs = config.getoption("doctestglob") or ["test*.txt"] + for glob in globs: + if path.check(fnmatch=glob): + return True + return False + + +class ReprFailDoctest(TerminalRepr): def __init__( self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]] ) -> None: - self.reprlocation_lines = reprlocation_lines - + self.reprlocation_lines = reprlocation_lines + def toterminal(self, tw: TerminalWriter) -> None: - for reprlocation, lines in self.reprlocation_lines: - for line in lines: - tw.line(line) - reprlocation.toterminal(tw) - - -class MultipleDoctestFailures(Exception): + for reprlocation, lines in self.reprlocation_lines: + for line in lines: + tw.line(line) + reprlocation.toterminal(tw) + + +class MultipleDoctestFailures(Exception): def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None: super().__init__() - self.failures = failures - - + self.failures = failures + + def _init_runner_class() -> Type["doctest.DocTestRunner"]: - import doctest - - class PytestDoctestRunner(doctest.DebugRunner): + import doctest + + class PytestDoctestRunner(doctest.DebugRunner): """Runner to collect failures. Note that the out variable in this case is a list instead of a stdout-like object. - """ - - def __init__( + """ + + def __init__( self, checker: Optional["doctest.OutputChecker"] = None, verbose: Optional[bool] = None, optionflags: int = 0, continue_on_failure: bool = True, ) -> None: - doctest.DebugRunner.__init__( - self, checker=checker, verbose=verbose, optionflags=optionflags - ) - self.continue_on_failure = continue_on_failure - + doctest.DebugRunner.__init__( + self, checker=checker, verbose=verbose, optionflags=optionflags + ) + self.continue_on_failure = continue_on_failure + def report_failure( self, out, test: "doctest.DocTest", example: "doctest.Example", got: str, ) -> None: - failure = doctest.DocTestFailure(test, example, got) - if self.continue_on_failure: - out.append(failure) - else: - raise failure - + failure = doctest.DocTestFailure(test, example, got) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + def report_unexpected_exception( self, out, @@ -210,36 +210,36 @@ def _init_runner_class() -> Type["doctest.DocTestRunner"]: raise exc_info[1] if isinstance(exc_info[1], bdb.BdbQuit): outcomes.exit("Quitting debugger") - failure = doctest.UnexpectedException(test, example, exc_info) - if self.continue_on_failure: - out.append(failure) - else: - raise failure - - return PytestDoctestRunner - - + failure = doctest.UnexpectedException(test, example, exc_info) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + return PytestDoctestRunner + + def _get_runner( checker: Optional["doctest.OutputChecker"] = None, verbose: Optional[bool] = None, optionflags: int = 0, continue_on_failure: bool = True, ) -> "doctest.DocTestRunner": - # We need this in order to do a lazy import on doctest - global RUNNER_CLASS - if RUNNER_CLASS is None: - RUNNER_CLASS = _init_runner_class() + # We need this in order to do a lazy import on doctest + global RUNNER_CLASS + if RUNNER_CLASS is None: + RUNNER_CLASS = _init_runner_class() # Type ignored because the continue_on_failure argument is only defined on # PytestDoctestRunner, which is lazily defined so can't be used as a type. return RUNNER_CLASS( # type: ignore - checker=checker, - verbose=verbose, - optionflags=optionflags, - continue_on_failure=continue_on_failure, - ) - - -class DoctestItem(pytest.Item): + checker=checker, + verbose=verbose, + optionflags=optionflags, + continue_on_failure=continue_on_failure, + ) + + +class DoctestItem(pytest.Item): def __init__( self, name: str, @@ -248,11 +248,11 @@ class DoctestItem(pytest.Item): dtest: Optional["doctest.DocTest"] = None, ) -> None: super().__init__(name, parent) - self.runner = runner - self.dtest = dtest - self.obj = None + self.runner = runner + self.dtest = dtest + self.obj = None self.fixture_request: Optional[FixtureRequest] = None - + @classmethod def from_parent( # type: ignore cls, @@ -267,187 +267,187 @@ class DoctestItem(pytest.Item): return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) def setup(self) -> None: - if self.dtest is not None: - self.fixture_request = _setup_fixtures(self) - globs = dict(getfixture=self.fixture_request.getfixturevalue) - for name, value in self.fixture_request.getfixturevalue( - "doctest_namespace" - ).items(): - globs[name] = value - self.dtest.globs.update(globs) - + if self.dtest is not None: + self.fixture_request = _setup_fixtures(self) + globs = dict(getfixture=self.fixture_request.getfixturevalue) + for name, value in self.fixture_request.getfixturevalue( + "doctest_namespace" + ).items(): + globs[name] = value + self.dtest.globs.update(globs) + def runtest(self) -> None: assert self.dtest is not None assert self.runner is not None - _check_all_skipped(self.dtest) - self._disable_output_capturing_for_darwin() + _check_all_skipped(self.dtest) + self._disable_output_capturing_for_darwin() failures: List["doctest.DocTestFailure"] = [] # Type ignored because we change the type of `out` from what # doctest expects. self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] - if failures: - raise MultipleDoctestFailures(failures) - + if failures: + raise MultipleDoctestFailures(failures) + def _disable_output_capturing_for_darwin(self) -> None: """Disable output capturing. Otherwise, stdout is lost to doctest (#985).""" - if platform.system() != "Darwin": - return - capman = self.config.pluginmanager.getplugin("capturemanager") - if capman: - capman.suspend_global_capture(in_=True) - out, err = capman.read_global_capture() - sys.stdout.write(out) - sys.stderr.write(err) - + if platform.system() != "Darwin": + return + capman = self.config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture(in_=True) + out, err = capman.read_global_capture() + sys.stdout.write(out) + sys.stderr.write(err) + # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException], ) -> Union[str, TerminalRepr]: - import doctest - + import doctest + failures: Optional[ Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]] ] = (None) if isinstance( excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException) ): - failures = [excinfo.value] + failures = [excinfo.value] elif isinstance(excinfo.value, MultipleDoctestFailures): - failures = excinfo.value.failures - - if failures is not None: - reprlocation_lines = [] - for failure in failures: - example = failure.example - test = failure.test - filename = test.filename - if test.lineno is None: - lineno = None - else: - lineno = test.lineno + example.lineno + 1 - message = type(failure).__name__ + failures = excinfo.value.failures + + if failures is not None: + reprlocation_lines = [] + for failure in failures: + example = failure.example + test = failure.test + filename = test.filename + if test.lineno is None: + lineno = None + else: + lineno = test.lineno + example.lineno + 1 + message = type(failure).__name__ # TODO: ReprFileLocation doesn't expect a None lineno. reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] - checker = _get_checker() - report_choice = _get_report_choice( - self.config.getoption("doctestreport") - ) - if lineno is not None: + checker = _get_checker() + report_choice = _get_report_choice( + self.config.getoption("doctestreport") + ) + if lineno is not None: assert failure.test.docstring is not None - lines = failure.test.docstring.splitlines(False) - # add line numbers to the left of the error message + lines = failure.test.docstring.splitlines(False) + # add line numbers to the left of the error message assert test.lineno is not None - lines = [ - "%03d %s" % (i + test.lineno + 1, x) - for (i, x) in enumerate(lines) - ] - # trim docstring error lines to 10 - lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] - else: - lines = [ - "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" - ] - indent = ">>>" - for line in example.source.splitlines(): + lines = [ + "%03d %s" % (i + test.lineno + 1, x) + for (i, x) in enumerate(lines) + ] + # trim docstring error lines to 10 + lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] + else: + lines = [ + "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example" + ] + indent = ">>>" + for line in example.source.splitlines(): lines.append(f"??? {indent} {line}") - indent = "..." - if isinstance(failure, doctest.DocTestFailure): - lines += checker.output_difference( - example, failure.got, report_choice - ).split("\n") - else: - inner_excinfo = ExceptionInfo(failure.exc_info) - lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] + indent = "..." + if isinstance(failure, doctest.DocTestFailure): + lines += checker.output_difference( + example, failure.got, report_choice + ).split("\n") + else: + inner_excinfo = ExceptionInfo(failure.exc_info) + lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] lines += [ x.strip("\n") for x in traceback.format_exception(*failure.exc_info) ] - reprlocation_lines.append((reprlocation, lines)) - return ReprFailDoctest(reprlocation_lines) - else: + reprlocation_lines.append((reprlocation, lines)) + return ReprFailDoctest(reprlocation_lines) + else: return super().repr_failure(excinfo) - + def reportinfo(self): assert self.dtest is not None - return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name - - + return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name + + def _get_flag_lookup() -> Dict[str, int]: - import doctest - - return dict( - DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1, - DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE, - NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, - ELLIPSIS=doctest.ELLIPSIS, - IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, - COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, - ALLOW_UNICODE=_get_allow_unicode_flag(), - ALLOW_BYTES=_get_allow_bytes_flag(), + import doctest + + return dict( + DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1, + DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE, + NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE, + ELLIPSIS=doctest.ELLIPSIS, + IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL, + COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, + ALLOW_UNICODE=_get_allow_unicode_flag(), + ALLOW_BYTES=_get_allow_bytes_flag(), NUMBER=_get_number_flag(), - ) - - -def get_optionflags(parent): - optionflags_str = parent.config.getini("doctest_optionflags") - flag_lookup_table = _get_flag_lookup() - flag_acc = 0 - for flag in optionflags_str: - flag_acc |= flag_lookup_table[flag] - return flag_acc - - -def _get_continue_on_failure(config): - continue_on_failure = config.getvalue("doctest_continue_on_failure") - if continue_on_failure: - # We need to turn off this if we use pdb since we should stop at + ) + + +def get_optionflags(parent): + optionflags_str = parent.config.getini("doctest_optionflags") + flag_lookup_table = _get_flag_lookup() + flag_acc = 0 + for flag in optionflags_str: + flag_acc |= flag_lookup_table[flag] + return flag_acc + + +def _get_continue_on_failure(config): + continue_on_failure = config.getvalue("doctest_continue_on_failure") + if continue_on_failure: + # We need to turn off this if we use pdb since we should stop at # the first failure. - if config.getvalue("usepdb"): - continue_on_failure = False - return continue_on_failure - - -class DoctestTextfile(pytest.Module): - obj = None - + if config.getvalue("usepdb"): + continue_on_failure = False + return continue_on_failure + + +class DoctestTextfile(pytest.Module): + obj = None + def collect(self) -> Iterable[DoctestItem]: - import doctest - + import doctest + # Inspired by doctest.testfile; ideally we would use it directly, # but it doesn't support passing a custom checker. - encoding = self.config.getini("doctest_encoding") - text = self.fspath.read_text(encoding) - filename = str(self.fspath) - name = self.fspath.basename - globs = {"__name__": "__main__"} - - optionflags = get_optionflags(self) - - runner = _get_runner( + encoding = self.config.getini("doctest_encoding") + text = self.fspath.read_text(encoding) + filename = str(self.fspath) + name = self.fspath.basename + globs = {"__name__": "__main__"} + + optionflags = get_optionflags(self) + + runner = _get_runner( verbose=False, - optionflags=optionflags, - checker=_get_checker(), - continue_on_failure=_get_continue_on_failure(self.config), - ) - - parser = doctest.DocTestParser() - test = parser.get_doctest(text, globs, name, filename, 0) - if test.examples: + optionflags=optionflags, + checker=_get_checker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + + parser = doctest.DocTestParser() + test = parser.get_doctest(text, globs, name, filename, 0) + if test.examples: yield DoctestItem.from_parent( self, name=test.name, runner=runner, dtest=test ) - - + + def _check_all_skipped(test: "doctest.DocTest") -> None: """Raise pytest.skip() if all examples in the given DocTest have the SKIP option set.""" - import doctest - - all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) - if all_skipped: - pytest.skip("all tests skipped by +SKIP option") - - + import doctest + + all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples) + if all_skipped: + pytest.skip("all tests skipped by +SKIP option") + + def _is_mocked(obj: object) -> bool: """Return if an object is possibly a mock object by checking the existence of a highly improbable attribute.""" @@ -487,10 +487,10 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]: inspect.unwrap = real_unwrap -class DoctestModule(pytest.Module): +class DoctestModule(pytest.Module): def collect(self) -> Iterable[DoctestItem]: - import doctest - + import doctest + class MockAwareDocTestFinder(doctest.DocTestFinder): """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug. @@ -523,62 +523,62 @@ class DoctestModule(pytest.Module): self, tests, obj, name, module, source_lines, globs, seen ) - if self.fspath.basename == "conftest.py": + if self.fspath.basename == "conftest.py": module = self.config.pluginmanager._importconftest( self.fspath, self.config.getoption("importmode") ) - else: - try: + else: + try: module = import_path(self.fspath) - except ImportError: - if self.config.getvalue("doctest_ignore_import_errors"): - pytest.skip("unable to import module %r" % self.fspath) - else: - raise + except ImportError: + if self.config.getvalue("doctest_ignore_import_errors"): + pytest.skip("unable to import module %r" % self.fspath) + else: + raise # Uses internal doctest module parsing mechanism. finder = MockAwareDocTestFinder() - optionflags = get_optionflags(self) - runner = _get_runner( + optionflags = get_optionflags(self) + runner = _get_runner( verbose=False, - optionflags=optionflags, - checker=_get_checker(), - continue_on_failure=_get_continue_on_failure(self.config), - ) - - for test in finder.find(module, module.__name__): - if test.examples: # skip empty doctests + optionflags=optionflags, + checker=_get_checker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + + for test in finder.find(module, module.__name__): + if test.examples: # skip empty doctests yield DoctestItem.from_parent( self, name=test.name, runner=runner, dtest=test ) - - + + def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest: """Used by DoctestTextfile and DoctestItem to setup fixture information.""" - + def func() -> None: - pass - + pass + doctest_item.funcargs = {} # type: ignore[attr-defined] - fm = doctest_item.session._fixturemanager + fm = doctest_item.session._fixturemanager doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] - node=doctest_item, func=func, cls=None, funcargs=False - ) + node=doctest_item, func=func, cls=None, funcargs=False + ) fixture_request = FixtureRequest(doctest_item, _ispytest=True) - fixture_request._fillfixtures() - return fixture_request - - + fixture_request._fillfixtures() + return fixture_request + + def _init_checker_class() -> Type["doctest.OutputChecker"]: - import doctest - import re - - class LiteralsOutputChecker(doctest.OutputChecker): + import doctest + import re + + class LiteralsOutputChecker(doctest.OutputChecker): # Based on doctest_nose_plugin.py from the nltk project # (https://github.com/nltk/nltk) and on the "numtest" doctest extension # by Sebastien Boisgerault (https://github.com/boisgera/numtest). - - _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) - _bytes_literal_re = re.compile(r"(\W|^)[bB]([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) _number_re = re.compile( r""" (?P<number> @@ -601,34 +601,34 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]: """, re.VERBOSE, ) - + def check_output(self, want: str, got: str, optionflags: int) -> bool: if doctest.OutputChecker.check_output(self, want, got, optionflags): - return True - - allow_unicode = optionflags & _get_allow_unicode_flag() - allow_bytes = optionflags & _get_allow_bytes_flag() + return True + + allow_unicode = optionflags & _get_allow_unicode_flag() + allow_bytes = optionflags & _get_allow_bytes_flag() allow_number = optionflags & _get_number_flag() if not allow_unicode and not allow_bytes and not allow_number: - return False - + return False + def remove_prefixes(regex: Pattern[str], txt: str) -> str: return re.sub(regex, r"\1\2", txt) - + if allow_unicode: want = remove_prefixes(self._unicode_literal_re, want) got = remove_prefixes(self._unicode_literal_re, got) - + if allow_bytes: want = remove_prefixes(self._bytes_literal_re, want) got = remove_prefixes(self._bytes_literal_re, got) - + if allow_number: got = self._remove_unwanted_precision(want, got) - + return doctest.OutputChecker.check_output(self, want, got, optionflags) - + def _remove_unwanted_precision(self, want: str, got: str) -> str: wants = list(self._number_re.finditer(want)) gots = list(self._number_re.finditer(got)) @@ -681,18 +681,18 @@ def _get_checker() -> "doctest.OutputChecker": def _get_allow_unicode_flag() -> int: """Register and return the ALLOW_UNICODE flag.""" - import doctest - - return doctest.register_optionflag("ALLOW_UNICODE") - - + import doctest + + return doctest.register_optionflag("ALLOW_UNICODE") + + def _get_allow_bytes_flag() -> int: """Register and return the ALLOW_BYTES flag.""" - import doctest - - return doctest.register_optionflag("ALLOW_BYTES") - - + import doctest + + return doctest.register_optionflag("ALLOW_BYTES") + + def _get_number_flag() -> int: """Register and return the NUMBER flag.""" import doctest @@ -706,19 +706,19 @@ def _get_report_choice(key: str) -> int: 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 - - return { - DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF, - DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF, - DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF, - DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE, - DOCTEST_REPORT_CHOICE_NONE: 0, - }[key] - - -@pytest.fixture(scope="session") + import doctest + + return { + DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF, + DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF, + DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF, + DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE, + DOCTEST_REPORT_CHOICE_NONE: 0, + }[key] + + +@pytest.fixture(scope="session") def doctest_namespace() -> Dict[str, Any]: """Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.""" - return dict() + return dict() diff --git a/contrib/python/pytest/py3/_pytest/fixtures.py b/contrib/python/pytest/py3/_pytest/fixtures.py index c793516256..273bcafd39 100644 --- a/contrib/python/pytest/py3/_pytest/fixtures.py +++ b/contrib/python/pytest/py3/_pytest/fixtures.py @@ -1,10 +1,10 @@ -import functools -import inspect +import functools +import inspect import os -import sys -import warnings -from collections import defaultdict -from collections import deque +import sys +import warnings +from collections import defaultdict +from collections import deque from types import TracebackType from typing import Any from typing import Callable @@ -24,28 +24,28 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - -import attr -import py - -import _pytest + +import attr +import py + +import _pytest from _pytest import nodes from _pytest._code import getfslineno from _pytest._code.code import FormattedExcinfo -from _pytest._code.code import TerminalRepr +from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import _format_args -from _pytest.compat import _PytestWrapper +from _pytest.compat import _format_args +from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never from _pytest.compat import final -from _pytest.compat import get_real_func -from _pytest.compat import get_real_method -from _pytest.compat import getfuncargnames -from _pytest.compat import getimfunc -from _pytest.compat import getlocation -from _pytest.compat import is_generator -from _pytest.compat import NOTSET -from _pytest.compat import safe_getattr +from _pytest.compat import get_real_func +from _pytest.compat import get_real_method +from _pytest.compat import getfuncargnames +from _pytest.compat import getimfunc +from _pytest.compat import getlocation +from _pytest.compat import is_generator +from _pytest.compat import NOTSET +from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config.argparsing import Parser @@ -55,16 +55,16 @@ from _pytest.deprecated import YIELD_FIXTURE from _pytest.mark import Mark from _pytest.mark import ParameterSet from _pytest.mark.structures import MarkDecorator -from _pytest.outcomes import fail -from _pytest.outcomes import TEST_OUTCOME +from _pytest.outcomes import fail +from _pytest.outcomes import TEST_OUTCOME from _pytest.pathlib import absolutepath from _pytest.store import StoreKey - + if TYPE_CHECKING: from typing import Deque from typing import NoReturn from typing_extensions import Literal - + from _pytest.main import Session from _pytest.python import CallSpec2 from _pytest.python import Function @@ -72,7 +72,7 @@ if TYPE_CHECKING: _Scope = Literal["session", "package", "module", "class", "function"] - + # The value of the fixture -- return/yield of the fixture function (type variable). _FixtureValue = TypeVar("_FixtureValue") # The type of the fixture function (type variable). @@ -98,38 +98,38 @@ _FixtureCachedResult = Union[ Tuple[Type[BaseException], BaseException, TracebackType], ], ] - - + + @attr.s(frozen=True) class PseudoFixtureDef(Generic[_FixtureValue]): cached_result = attr.ib(type="_FixtureCachedResult[_FixtureValue]") scope = attr.ib(type="_Scope") - - + + def pytest_sessionstart(session: "Session") -> None: session._fixturemanager = FixtureManager(session) - - + + def get_scope_package(node, fixturedef: "FixtureDef[object]"): - import pytest - - cls = pytest.Package - current = node + import pytest + + cls = pytest.Package + current = node fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") - while current and ( - type(current) is not cls or fixture_package_name != current.nodeid - ): - current = current.parent - if current is None: - return node.session - return current - - + while current and ( + type(current) is not cls or fixture_package_name != current.nodeid + ): + current = current.parent + if current is None: + return node.session + return current + + def get_scope_node( node: nodes.Node, scope: "_Scope" ) -> Optional[Union[nodes.Item, nodes.Collector]]: import _pytest.python - + if scope == "function": return node.getparent(nodes.Item) elif scope == "class": @@ -142,7 +142,7 @@ def get_scope_node( return node.getparent(_pytest.main.Session) else: assert_never(scope) - + # Used for storing artificial fixturedefs for direct parametrization. name2pseudofixturedef_key = StoreKey[Dict[str, "FixtureDef[Any]"]]() @@ -152,48 +152,48 @@ def add_funcarg_pseudo_fixture_def( collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager" ) -> None: # This function will transform all collected calls to functions - # if they use direct funcargs (i.e. direct parametrization) - # because we want later test execution to be able to rely on - # an existing FixtureDef structure for all arguments. - # XXX we can probably avoid this algorithm if we modify CallSpec2 - # to directly care for creating the fixturedefs within its methods. - if not metafunc._calls[0].funcargs: + # if they use direct funcargs (i.e. direct parametrization) + # because we want later test execution to be able to rely on + # an existing FixtureDef structure for all arguments. + # XXX we can probably avoid this algorithm if we modify CallSpec2 + # to directly care for creating the fixturedefs within its methods. + if not metafunc._calls[0].funcargs: # This function call does not have direct parametrization. return # Collect funcargs of all callspecs into a list of values. arg2params: Dict[str, List[object]] = {} arg2scope: Dict[str, _Scope] = {} - for callspec in metafunc._calls: - for argname, argvalue in callspec.funcargs.items(): - assert argname not in callspec.params - callspec.params[argname] = argvalue - arg2params_list = arg2params.setdefault(argname, []) - callspec.indices[argname] = len(arg2params_list) - arg2params_list.append(argvalue) - if argname not in arg2scope: - scopenum = callspec._arg2scopenum.get(argname, scopenum_function) - arg2scope[argname] = scopes[scopenum] - callspec.funcargs.clear() - + for callspec in metafunc._calls: + for argname, argvalue in callspec.funcargs.items(): + assert argname not in callspec.params + callspec.params[argname] = argvalue + arg2params_list = arg2params.setdefault(argname, []) + callspec.indices[argname] = len(arg2params_list) + arg2params_list.append(argvalue) + if argname not in arg2scope: + scopenum = callspec._arg2scopenum.get(argname, scopenum_function) + arg2scope[argname] = scopes[scopenum] + callspec.funcargs.clear() + # Register artificial FixtureDef's so that later at test execution - # time we can rely on a proper FixtureDef to exist for fixture setup. - arg2fixturedefs = metafunc._arg2fixturedefs - for argname, valuelist in arg2params.items(): + # time we can rely on a proper FixtureDef to exist for fixture setup. + arg2fixturedefs = metafunc._arg2fixturedefs + for argname, valuelist in arg2params.items(): # If we have a scope that is higher than function, we need - # to make sure we only ever create an according fixturedef on - # a per-scope basis. We thus store and cache the fixturedef on the - # node related to the scope. - scope = arg2scope[argname] - node = None - if scope != "function": - node = get_scope_node(collector, scope) - if node is None: - assert scope == "class" and isinstance(collector, _pytest.python.Module) + # to make sure we only ever create an according fixturedef on + # a per-scope basis. We thus store and cache the fixturedef on the + # node related to the scope. + scope = arg2scope[argname] + node = None + if scope != "function": + node = get_scope_node(collector, scope) + if node is None: + assert scope == "class" and isinstance(collector, _pytest.python.Module) # Use module-level collector for class-scope (for now). - node = collector + node = collector if node is None: name2pseudofixturedef = None - else: + else: default: Dict[str, FixtureDef[Any]] = {} name2pseudofixturedef = node._store.setdefault( name2pseudofixturedef_key, default @@ -201,7 +201,7 @@ def add_funcarg_pseudo_fixture_def( if name2pseudofixturedef is not None and argname in name2pseudofixturedef: arg2fixturedefs[argname] = [name2pseudofixturedef[argname]] else: - fixturedef = FixtureDef( + fixturedef = FixtureDef( fixturemanager=fixturemanager, baseid="", argname=argname, @@ -210,138 +210,138 @@ def add_funcarg_pseudo_fixture_def( params=valuelist, unittest=False, ids=None, - ) - arg2fixturedefs[argname] = [fixturedef] + ) + arg2fixturedefs[argname] = [fixturedef] if name2pseudofixturedef is not None: name2pseudofixturedef[argname] = fixturedef - - + + def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: """Return fixturemarker or None if it doesn't exist or raised - exceptions.""" - try: + exceptions.""" + try: fixturemarker: Optional[FixtureFunctionMarker] = getattr( obj, "_pytestfixturefunction", None ) - except TEST_OUTCOME: - # some objects raise errors like request (from flask import request) - # we don't expect them to be fixture functions - return None + except TEST_OUTCOME: + # some objects raise errors like request (from flask import request) + # we don't expect them to be fixture functions + return None return fixturemarker - - + + # Parametrized fixture key, helper alias for code below. _Key = Tuple[object, ...] def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_Key]: """Return list of keys for all parametrized arguments which match - the specified scope. """ - assert scopenum < scopenum_function # function - try: + the specified scope. """ + assert scopenum < scopenum_function # function + try: callspec = item.callspec # type: ignore[attr-defined] - except AttributeError: - pass - else: + except AttributeError: + pass + else: cs: CallSpec2 = callspec - # cs.indices.items() is random order of argnames. Need to - # sort this so that different calls to - # get_parametrized_fixture_keys will be deterministic. - for argname, param_index in sorted(cs.indices.items()): - if cs._arg2scopenum[argname] != scopenum: - continue - if scopenum == 0: # session + # cs.indices.items() is random order of argnames. Need to + # sort this so that different calls to + # get_parametrized_fixture_keys will be deterministic. + for argname, param_index in sorted(cs.indices.items()): + if cs._arg2scopenum[argname] != scopenum: + continue + if scopenum == 0: # session key: _Key = (argname, param_index) - elif scopenum == 1: # package - key = (argname, param_index, item.fspath.dirpath()) - elif scopenum == 2: # module - key = (argname, param_index, item.fspath) - elif scopenum == 3: # class + elif scopenum == 1: # package + key = (argname, param_index, item.fspath.dirpath()) + elif scopenum == 2: # module + key = (argname, param_index, item.fspath) + elif scopenum == 3: # class item_cls = item.cls # type: ignore[attr-defined] key = (argname, param_index, item.fspath, item_cls) - yield key - - + yield key + + # Algorithm for sorting on a per-parametrized resource setup basis. # 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. - - + + def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]] = {} items_by_argkey: Dict[int, Dict[_Key, Deque[nodes.Item]]] = {} - for scopenum in range(0, scopenum_function): + for scopenum in range(0, scopenum_function): d: Dict[nodes.Item, Dict[_Key, None]] = {} argkeys_cache[scopenum] = d item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque) items_by_argkey[scopenum] = item_d - for item in items: + for item in items: keys = dict.fromkeys(get_parametrized_fixture_keys(item, scopenum), None) - if keys: - d[item] = keys - for key in keys: - item_d[key].append(item) + if keys: + d[item] = keys + for key in keys: + item_d[key].append(item) items_dict = dict.fromkeys(items, None) return list(reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, 0)) - - + + def fix_cache_order( item: nodes.Item, argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]], items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]], ) -> None: - for scopenum in range(0, scopenum_function): - for key in argkeys_cache[scopenum].get(item, []): - items_by_argkey[scopenum][key].appendleft(item) - - + for scopenum in range(0, scopenum_function): + for key in argkeys_cache[scopenum].get(item, []): + items_by_argkey[scopenum][key].appendleft(item) + + def reorder_items_atscope( items: Dict[nodes.Item, None], argkeys_cache: Dict[int, Dict[nodes.Item, Dict[_Key, None]]], items_by_argkey: Dict[int, Dict[_Key, "Deque[nodes.Item]"]], scopenum: int, ) -> Dict[nodes.Item, None]: - if scopenum >= scopenum_function or len(items) < 3: - return items + if scopenum >= scopenum_function or len(items) < 3: + return items ignore: Set[Optional[_Key]] = set() - items_deque = deque(items) + items_deque = deque(items) items_done: Dict[nodes.Item, None] = {} - scoped_items_by_argkey = items_by_argkey[scopenum] - scoped_argkeys_cache = argkeys_cache[scopenum] - while items_deque: + scoped_items_by_argkey = items_by_argkey[scopenum] + scoped_argkeys_cache = argkeys_cache[scopenum] + while items_deque: no_argkey_group: Dict[nodes.Item, None] = {} - slicing_argkey = None - while items_deque: - item = items_deque.popleft() - if item in items_done or item in no_argkey_group: - continue + slicing_argkey = None + while items_deque: + item = items_deque.popleft() + if item in items_done or item in no_argkey_group: + continue argkeys = dict.fromkeys( (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None - ) - if not argkeys: - no_argkey_group[item] = None - else: - slicing_argkey, _ = argkeys.popitem() + ) + if not argkeys: + no_argkey_group[item] = None + else: + slicing_argkey, _ = argkeys.popitem() # We don't have to remove relevant items from later in the # deque because they'll just be ignored. - matching_items = [ - i for i in scoped_items_by_argkey[slicing_argkey] if i in items - ] - for i in reversed(matching_items): - fix_cache_order(i, argkeys_cache, items_by_argkey) - items_deque.appendleft(i) - break - if no_argkey_group: - no_argkey_group = reorder_items_atscope( - no_argkey_group, argkeys_cache, items_by_argkey, scopenum + 1 - ) - for item in no_argkey_group: - items_done[item] = None - ignore.add(slicing_argkey) - return items_done - - + matching_items = [ + i for i in scoped_items_by_argkey[slicing_argkey] if i in items + ] + for i in reversed(matching_items): + fix_cache_order(i, argkeys_cache, items_by_argkey) + items_deque.appendleft(i) + break + if no_argkey_group: + no_argkey_group = reorder_items_atscope( + no_argkey_group, argkeys_cache, items_by_argkey, scopenum + 1 + ) + for item in no_argkey_group: + items_done[item] = None + ignore.add(slicing_argkey) + return items_done + + def _fillfuncargs(function: "Function") -> None: """Fill missing fixtures for a test function, old public API (deprecated).""" warnings.warn(FILLFUNCARGS.format(name="pytest._fillfuncargs()"), stacklevel=2) @@ -358,162 +358,162 @@ def fillfixtures(function: "Function") -> None: def _fill_fixtures_impl(function: "Function") -> None: """Internal implementation to fill fixtures on the given function object.""" - try: - request = function._request - except AttributeError: - # XXX this special code path is only expected to execute - # with the oejskit plugin. It uses classes with funcargs - # and we thus have to work a bit to allow this. - fm = function.session._fixturemanager + try: + request = function._request + except AttributeError: + # XXX this special code path is only expected to execute + # with the oejskit plugin. It uses classes with funcargs + # and we thus have to work a bit to allow this. + fm = function.session._fixturemanager assert function.parent is not None - fi = fm.getfixtureinfo(function.parent, function.obj, None) - function._fixtureinfo = fi + fi = fm.getfixtureinfo(function.parent, function.obj, None) + function._fixtureinfo = fi request = function._request = FixtureRequest(function, _ispytest=True) - request._fillfixtures() + request._fillfixtures() # Prune out funcargs for jstests. - newfuncargs = {} - for name in fi.argnames: - newfuncargs[name] = function.funcargs[name] - function.funcargs = newfuncargs - else: - request._fillfixtures() - - -def get_direct_param_fixture_func(request): - return request.param - - -@attr.s(slots=True) + newfuncargs = {} + for name in fi.argnames: + newfuncargs[name] = function.funcargs[name] + function.funcargs = newfuncargs + else: + request._fillfixtures() + + +def get_direct_param_fixture_func(request): + return request.param + + +@attr.s(slots=True) class FuncFixtureInfo: # Original function argument names. argnames = attr.ib(type=Tuple[str, ...]) # Argnames that function immediately requires. These include argnames + - # fixture names specified via usefixtures and via autouse=True in fixture - # definitions. + # fixture names specified via usefixtures and via autouse=True in fixture + # definitions. initialnames = attr.ib(type=Tuple[str, ...]) names_closure = attr.ib(type=List[str]) name2fixturedefs = attr.ib(type=Dict[str, Sequence["FixtureDef[Any]"]]) - + def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs. - - Can only reduce names_closure, which means that the new closure will - always be a subset of the old one. The order is preserved. - - This method is needed because direct parametrization may shadow some - of the fixtures that were included in the originally built dependency - tree. In this way the dependency tree can get pruned, and the closure - of argnames may get reduced. - """ + + Can only reduce names_closure, which means that the new closure will + always be a subset of the old one. The order is preserved. + + This method is needed because direct parametrization may shadow some + of the fixtures that were included in the originally built dependency + tree. In this way the dependency tree can get pruned, and the closure + of argnames may get reduced. + """ closure: Set[str] = set() - working_set = set(self.initialnames) - while working_set: - argname = working_set.pop() + working_set = set(self.initialnames) + while working_set: + argname = working_set.pop() # Argname may be smth not included in the original names_closure, - # in which case we ignore it. This currently happens with pseudo - # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'. - # So they introduce the new dependency 'request' which might have - # been missing in the original tree (closure). - if argname not in closure and argname in self.names_closure: - closure.add(argname) - if argname in self.name2fixturedefs: - working_set.update(self.name2fixturedefs[argname][-1].argnames) - - self.names_closure[:] = sorted(closure, key=self.names_closure.index) - - + # in which case we ignore it. This currently happens with pseudo + # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'. + # So they introduce the new dependency 'request' which might have + # been missing in the original tree (closure). + if argname not in closure and argname in self.names_closure: + closure.add(argname) + if argname in self.name2fixturedefs: + working_set.update(self.name2fixturedefs[argname][-1].argnames) + + self.names_closure[:] = sorted(closure, key=self.names_closure.index) + + class FixtureRequest: """A request for a fixture from a test or fixture function. - + A request object gives access to the requesting test context and has an optional ``param`` attribute in case the fixture is parametrized indirectly. - """ - + """ + def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) - self._pyfuncitem = pyfuncitem + self._pyfuncitem = pyfuncitem #: Fixture for which this request is being performed. self.fixturename: Optional[str] = None #: Scope string, one of "function", "class", "module", "session". self.scope: _Scope = "function" self._fixture_defs: Dict[str, FixtureDef[Any]] = {} fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo - self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() + self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index: Dict[str, int] = {} self._fixturemanager: FixtureManager = (pyfuncitem.session._fixturemanager) - - @property + + @property def fixturenames(self) -> List[str]: """Names of all active fixtures in this request.""" - result = list(self._pyfuncitem._fixtureinfo.names_closure) - result.extend(set(self._fixture_defs).difference(result)) - return result - - @property - def node(self): + result = list(self._pyfuncitem._fixtureinfo.names_closure) + result.extend(set(self._fixture_defs).difference(result)) + return result + + @property + def node(self): """Underlying collection node (depends on current request scope).""" - return self._getscopeitem(self.scope) - + return self._getscopeitem(self.scope) + def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": - fixturedefs = self._arg2fixturedefs.get(argname, None) - if fixturedefs is None: + fixturedefs = self._arg2fixturedefs.get(argname, None) + if fixturedefs is None: # 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. assert self._pyfuncitem.parent is not None - parentid = self._pyfuncitem.parent.nodeid - fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) + parentid = self._pyfuncitem.parent.nodeid + fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) # TODO: Fix this type ignore. Either add assert or adjust types. # Can this be None here? self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] # fixturedefs list is immutable so we maintain a decreasing index. - index = self._arg2index.get(argname, 0) - 1 - if fixturedefs is None or (-index > len(fixturedefs)): - raise FixtureLookupError(argname, self) - self._arg2index[argname] = index - return fixturedefs[index] - - @property + index = self._arg2index.get(argname, 0) - 1 + if fixturedefs is None or (-index > len(fixturedefs)): + raise FixtureLookupError(argname, self) + self._arg2index[argname] = index + return fixturedefs[index] + + @property def config(self) -> Config: """The pytest config object associated with this request.""" return self._pyfuncitem.config # type: ignore[no-any-return] - + @property - def function(self): + def function(self): """Test function object if the request has a per-function scope.""" if self.scope != "function": raise AttributeError( f"function not available in {self.scope}-scoped context" ) - return self._pyfuncitem.obj - + return self._pyfuncitem.obj + @property - def cls(self): + def cls(self): """Class (can be None) where the test function was collected.""" if self.scope not in ("class", "function"): raise AttributeError(f"cls not available in {self.scope}-scoped context") - clscol = self._pyfuncitem.getparent(_pytest.python.Class) - if clscol: - return clscol.obj - - @property - def instance(self): + clscol = self._pyfuncitem.getparent(_pytest.python.Class) + if clscol: + return clscol.obj + + @property + def instance(self): """Instance (can be None) on which test function was collected.""" # unittest support hack, see _pytest.unittest.TestCaseFunction. - try: - return self._pyfuncitem._testcase - except AttributeError: - function = getattr(self, "function", None) - return getattr(function, "__self__", None) - + try: + return self._pyfuncitem._testcase + except AttributeError: + function = getattr(self, "function", None) + return getattr(function, "__self__", None) + @property - def module(self): + def module(self): """Python module object where the test function was collected.""" if self.scope not in ("function", "class", "module"): raise AttributeError(f"module not available in {self.scope}-scoped context") - return self._pyfuncitem.getparent(_pytest.python.Module).obj - + return self._pyfuncitem.getparent(_pytest.python.Module).obj + @property def fspath(self) -> py.path.local: """The file system path of the test module which collected this test.""" @@ -521,99 +521,99 @@ class FixtureRequest: raise AttributeError(f"module not available in {self.scope}-scoped context") # TODO: Remove ignore once _pyfuncitem is properly typed. return self._pyfuncitem.fspath # type: ignore - - @property - def keywords(self): + + @property + def keywords(self): """Keywords/markers dictionary for the underlying node.""" - return self.node.keywords - - @property + return self.node.keywords + + @property def session(self) -> "Session": """Pytest session object.""" return self._pyfuncitem.session # type: ignore[no-any-return] - + def addfinalizer(self, finalizer: Callable[[], object]) -> None: """Add finalizer/teardown function to be called after the last test within the requesting test context finished execution.""" # 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: - colitem = self._getscopeitem(scope) - self._pyfuncitem.session._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem - ) - + colitem = self._getscopeitem(scope) + self._pyfuncitem.session._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem + ) + def applymarker(self, marker: Union[str, MarkDecorator]) -> None: """Apply a marker to a single test function invocation. - This method is useful if you don't want to have a keyword/marker - on all function invocations. - + This method is useful if you don't want to have a keyword/marker + on all function invocations. + :param marker: 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": """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: - item = self._pyfuncitem - fixturenames = getattr(item, "fixturenames", self.fixturenames) - for argname in fixturenames: - if argname not in item.funcargs: - item.funcargs[argname] = self.getfixturevalue(argname) - + item = self._pyfuncitem + fixturenames = getattr(item, "fixturenames", self.fixturenames) + for argname in fixturenames: + if argname not in item.funcargs: + item.funcargs[argname] = self.getfixturevalue(argname) + def getfixturevalue(self, argname: str) -> Any: """Dynamically run a named fixture function. - - Declaring fixtures via function argument is recommended where possible. - 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 - or test function body. + + Declaring fixtures via function argument is recommended where possible. + 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 + or test function body. :raises pytest.FixtureLookupError: If the given fixture could not be found. - """ + """ fixturedef = self._get_active_fixturedef(argname) assert fixturedef.cached_result is not None return fixturedef.cached_result[0] - + def _get_active_fixturedef( self, argname: str ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: - try: - return self._fixture_defs[argname] - except KeyError: - try: - fixturedef = self._getnextfixturedef(argname) - except FixtureLookupError: - if argname == "request": - cached_result = (self, [0], None) + try: + return self._fixture_defs[argname] + except KeyError: + try: + fixturedef = self._getnextfixturedef(argname) + except FixtureLookupError: + if argname == "request": + cached_result = (self, [0], None) scope: _Scope = "function" - return PseudoFixtureDef(cached_result, scope) - raise + return PseudoFixtureDef(cached_result, scope) + raise # Remove indent to prevent the python3 exception # from leaking into the call. - self._compute_fixture_value(fixturedef) - self._fixture_defs[argname] = fixturedef - return fixturedef - + self._compute_fixture_value(fixturedef) + self._fixture_defs[argname] = fixturedef + return fixturedef + def _get_fixturestack(self) -> List["FixtureDef[Any]"]: - current = self + current = self values: List[FixtureDef[Any]] = [] - while 1: - fixturedef = getattr(current, "_fixturedef", None) - if fixturedef is None: - values.reverse() - return values - values.append(fixturedef) + while 1: + fixturedef = getattr(current, "_fixturedef", None) + if fixturedef is None: + values.reverse() + return values + values.append(fixturedef) assert isinstance(current, SubRequest) - current = current._parent_request - + current = current._parent_request + def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: """Create a SubRequest based on "self" and call the execute method of the given FixtureDef object. @@ -621,73 +621,73 @@ class FixtureRequest: 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 - # (latter managed by fixturedef) - argname = fixturedef.argname - funcitem = self._pyfuncitem - scope = fixturedef.scope - try: - param = funcitem.callspec.getparam(argname) - except (AttributeError, ValueError): - param = NOTSET - param_index = 0 - has_params = fixturedef.params is not None - fixtures_not_supported = getattr(funcitem, "nofuncargs", False) - if has_params and fixtures_not_supported: - msg = ( - "{name} does not support fixtures, maybe unittest.TestCase subclass?\n" - "Node id: {nodeid}\n" - "Function type: {typename}" - ).format( - name=funcitem.name, - nodeid=funcitem.nodeid, - typename=type(funcitem).__name__, - ) - fail(msg, pytrace=False) - if has_params: - frame = inspect.stack()[3] - frameinfo = inspect.getframeinfo(frame[0]) + """ + # prepare a subrequest object before calling fixture function + # (latter managed by fixturedef) + argname = fixturedef.argname + funcitem = self._pyfuncitem + scope = fixturedef.scope + try: + param = funcitem.callspec.getparam(argname) + except (AttributeError, ValueError): + param = NOTSET + param_index = 0 + has_params = fixturedef.params is not None + fixtures_not_supported = getattr(funcitem, "nofuncargs", False) + if has_params and fixtures_not_supported: + msg = ( + "{name} does not support fixtures, maybe unittest.TestCase subclass?\n" + "Node id: {nodeid}\n" + "Function type: {typename}" + ).format( + name=funcitem.name, + nodeid=funcitem.nodeid, + typename=type(funcitem).__name__, + ) + fail(msg, pytrace=False) + if has_params: + frame = inspect.stack()[3] + frameinfo = inspect.getframeinfo(frame[0]) source_path = py.path.local(frameinfo.filename) - source_lineno = frameinfo.lineno + source_lineno = frameinfo.lineno rel_source_path = source_path.relto(funcitem.config.rootdir) if rel_source_path: source_path_str = rel_source_path else: source_path_str = str(source_path) - msg = ( - "The requested fixture has no parameter defined for test:\n" - " {}\n\n" - "Requested fixture '{}' defined in:\n{}" - "\n\nRequested here:\n{}:{}".format( - funcitem.nodeid, - fixturedef.argname, - getlocation(fixturedef.func, funcitem.config.rootdir), + msg = ( + "The requested fixture has no parameter defined for test:\n" + " {}\n\n" + "Requested fixture '{}' defined in:\n{}" + "\n\nRequested here:\n{}:{}".format( + funcitem.nodeid, + fixturedef.argname, + getlocation(fixturedef.func, funcitem.config.rootdir), source_path_str, - source_lineno, - ) - ) - fail(msg, pytrace=False) - else: + source_lineno, + ) + ) + fail(msg, pytrace=False) + else: param_index = funcitem.callspec.indices[argname] # If a parametrize invocation set a scope it will override # the static scope defined with the fixture function. - paramscopenum = funcitem.callspec._arg2scopenum.get(argname) - if paramscopenum is not None: - scope = scopes[paramscopenum] - + paramscopenum = funcitem.callspec._arg2scopenum.get(argname) + if paramscopenum is not None: + scope = scopes[paramscopenum] + subrequest = SubRequest( self, scope, param, param_index, fixturedef, _ispytest=True ) - + # Check if a higher-level scoped fixture accesses a lower level one. - subrequest._check_scope(argname, self.scope, scope) - try: + subrequest._check_scope(argname, self.scope, scope) + try: # Call the fixture function. - fixturedef.execute(request=subrequest) - finally: + fixturedef.execute(request=subrequest) + finally: self._schedule_finalizers(fixturedef, subrequest) - + def _schedule_finalizers( self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" ) -> None: @@ -699,55 +699,55 @@ class FixtureRequest: def _check_scope( self, argname: str, invoking_scope: "_Scope", requested_scope: "_Scope", ) -> None: - if argname == "request": - return - if scopemismatch(invoking_scope, requested_scope): + if argname == "request": + return + if scopemismatch(invoking_scope, requested_scope): # Try to report something helpful. - lines = self._factorytraceback() - fail( - "ScopeMismatch: You tried to access the %r scoped " - "fixture %r with a %r scoped request object, " - "involved factories\n%s" - % ((requested_scope, argname, invoking_scope, "\n".join(lines))), - pytrace=False, - ) - + lines = self._factorytraceback() + fail( + "ScopeMismatch: You tried to access the %r scoped " + "fixture %r with a %r scoped request object, " + "involved factories\n%s" + % ((requested_scope, argname, invoking_scope, "\n".join(lines))), + pytrace=False, + ) + def _factorytraceback(self) -> List[str]: - lines = [] - for fixturedef in self._get_fixturestack(): - factory = fixturedef.func - fs, lineno = getfslineno(factory) - p = self._pyfuncitem.session.fspath.bestrelpath(fs) - args = _format_args(factory) + lines = [] + for fixturedef in self._get_fixturestack(): + factory = fixturedef.func + fs, lineno = getfslineno(factory) + p = self._pyfuncitem.session.fspath.bestrelpath(fs) + args = _format_args(factory) lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) - return lines - + return lines + def _getscopeitem(self, scope: "_Scope") -> Union[nodes.Item, nodes.Collector]: - if scope == "function": + if scope == "function": # This might also be a non-function Item despite its attribute name. node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem elif scope == "package": # FIXME: _fixturedef is not defined on FixtureRequest (this class), # but on FixtureRequest (a subclass). node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined] - else: - node = get_scope_node(self._pyfuncitem, scope) - if node is None and scope == "class": + else: + node = get_scope_node(self._pyfuncitem, scope) + if node is None and scope == "class": # Fallback to function item itself. - node = self._pyfuncitem - assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format( - scope, self._pyfuncitem - ) - return node - + node = self._pyfuncitem + assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format( + scope, self._pyfuncitem + ) + return node + def __repr__(self) -> str: - return "<FixtureRequest for %r>" % (self.node) - - + return "<FixtureRequest for %r>" % (self.node) + + @final -class SubRequest(FixtureRequest): +class SubRequest(FixtureRequest): """A sub request for handling getting a fixture from a test function/fixture.""" - + def __init__( self, request: "FixtureRequest", @@ -759,27 +759,27 @@ class SubRequest(FixtureRequest): _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) - self._parent_request = request - self.fixturename = fixturedef.argname - if param is not NOTSET: - self.param = param - self.param_index = param_index - self.scope = scope - self._fixturedef = fixturedef - self._pyfuncitem = request._pyfuncitem - self._fixture_defs = request._fixture_defs - self._arg2fixturedefs = request._arg2fixturedefs - self._arg2index = request._arg2index - self._fixturemanager = request._fixturemanager - + self._parent_request = request + self.fixturename = fixturedef.argname + if param is not NOTSET: + self.param = param + self.param_index = param_index + self.scope = scope + self._fixturedef = fixturedef + self._pyfuncitem = request._pyfuncitem + self._fixture_defs = request._fixture_defs + self._arg2fixturedefs = request._arg2fixturedefs + self._arg2index = request._arg2index + self._fixturemanager = request._fixturemanager + def __repr__(self) -> str: return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>" - + def addfinalizer(self, finalizer: Callable[[], object]) -> None: """Add finalizer/teardown function to be called after the last test within the requesting test context finished execution.""" - self._fixturedef.addfinalizer(finalizer) - + self._fixturedef.addfinalizer(finalizer) + def _schedule_finalizers( self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" ) -> None: @@ -791,89 +791,89 @@ class SubRequest(FixtureRequest): functools.partial(self._fixturedef.finish, request=self) ) super()._schedule_finalizers(fixturedef, subrequest) - - + + scopes: List["_Scope"] = ["session", "package", "module", "class", "function"] -scopenum_function = scopes.index("function") - - +scopenum_function = scopes.index("function") + + def scopemismatch(currentscope: "_Scope", newscope: "_Scope") -> bool: - return scopes.index(newscope) > scopes.index(currentscope) - - + return scopes.index(newscope) > scopes.index(currentscope) + + 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.""" strscopes: Sequence[str] = scopes - try: + try: return strscopes.index(scope) - except ValueError: - fail( - "{} {}got an unexpected scope value '{}'".format( + except ValueError: + fail( + "{} {}got an unexpected scope value '{}'".format( descr, f"from {where} " if where else "", scope - ), - pytrace=False, - ) - - + ), + pytrace=False, + ) + + @final -class FixtureLookupError(LookupError): +class FixtureLookupError(LookupError): """Could not return a requested fixture (missing or invalid).""" - + def __init__( self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None ) -> None: - self.argname = argname - self.request = request - self.fixturestack = request._get_fixturestack() - self.msg = msg - + self.argname = argname + self.request = request + self.fixturestack = request._get_fixturestack() + self.msg = msg + def formatrepr(self) -> "FixtureLookupErrorRepr": tblines: List[str] = [] - addline = tblines.append - stack = [self.request._pyfuncitem.obj] - stack.extend(map(lambda x: x.func, self.fixturestack)) - msg = self.msg - if msg is not None: + addline = tblines.append + stack = [self.request._pyfuncitem.obj] + stack.extend(map(lambda x: x.func, self.fixturestack)) + msg = self.msg + if msg is not None: # The last fixture raise an error, let's present # it at the requesting side. - stack = stack[:-1] - for function in stack: - fspath, lineno = getfslineno(function) - try: - lines, _ = inspect.getsourcelines(get_real_func(function)) + stack = stack[:-1] + for function in stack: + fspath, lineno = getfslineno(function) + try: + lines, _ = inspect.getsourcelines(get_real_func(function)) except (OSError, IndexError, TypeError): - error_msg = "file %s, line %s: source code not available" - addline(error_msg % (fspath, lineno + 1)) - else: + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno + 1)) + else: addline("file {}, line {}".format(fspath, lineno + 1)) - for i, line in enumerate(lines): - line = line.rstrip() - addline(" " + line) - if line.lstrip().startswith("def"): - break - - if msg is None: - fm = self.request._fixturemanager - available = set() - parentid = self.request._pyfuncitem.parent.nodeid - for name, fixturedefs in fm._arg2fixturedefs.items(): - faclist = list(fm._matchfactories(fixturedefs, parentid)) - if faclist: - available.add(name) - if self.argname in available: - msg = " recursive dependency involving fixture '{}' detected".format( - self.argname - ) - else: + for i, line in enumerate(lines): + line = line.rstrip() + addline(" " + line) + if line.lstrip().startswith("def"): + break + + if msg is None: + fm = self.request._fixturemanager + available = set() + parentid = self.request._pyfuncitem.parent.nodeid + for name, fixturedefs in fm._arg2fixturedefs.items(): + faclist = list(fm._matchfactories(fixturedefs, parentid)) + if faclist: + available.add(name) + if self.argname in available: + msg = " recursive dependency involving fixture '{}' detected".format( + self.argname + ) + else: msg = f"fixture '{self.argname}' not found" - msg += "\n available fixtures: {}".format(", ".join(sorted(available))) - msg += "\n use 'pytest --fixtures [testpath]' for help on them." - - return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) - - -class FixtureLookupErrorRepr(TerminalRepr): + msg += "\n available fixtures: {}".format(", ".join(sorted(available))) + msg += "\n use 'pytest --fixtures [testpath]' for help on them." + + return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) + + +class FixtureLookupErrorRepr(TerminalRepr): def __init__( self, filename: Union[str, py.path.local], @@ -882,37 +882,37 @@ class FixtureLookupErrorRepr(TerminalRepr): errorstring: str, argname: Optional[str], ) -> None: - self.tblines = tblines - self.errorstring = errorstring - self.filename = filename - self.firstlineno = firstlineno - self.argname = argname - + self.tblines = tblines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + self.argname = argname + def toterminal(self, tw: TerminalWriter) -> None: - # tw.line("FixtureLookupError: %s" %(self.argname), red=True) - for tbline in self.tblines: - tw.line(tbline.rstrip()) - lines = self.errorstring.split("\n") - if lines: - tw.line( - "{} {}".format(FormattedExcinfo.fail_marker, lines[0].strip()), - red=True, - ) - for line in lines[1:]: - tw.line( + # tw.line("FixtureLookupError: %s" %(self.argname), red=True) + for tbline in self.tblines: + tw.line(tbline.rstrip()) + lines = self.errorstring.split("\n") + if lines: + tw.line( + "{} {}".format(FormattedExcinfo.fail_marker, lines[0].strip()), + red=True, + ) + for line in lines[1:]: + tw.line( f"{FormattedExcinfo.flow_marker} {line.strip()}", red=True, - ) - tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) - - + ) + tw.line() + tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) + + def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn": - fs, lineno = getfslineno(fixturefunc) + fs, lineno = getfslineno(fixturefunc) location = "{}:{}".format(fs, lineno + 1) - source = _pytest._code.Source(fixturefunc) - fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) - - + source = _pytest._code.Source(fixturefunc) + fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) + + def call_fixture_func( fixturefunc: "_FixtureFunc[_FixtureValue]", request: FixtureRequest, kwargs ) -> _FixtureValue: @@ -926,25 +926,25 @@ def call_fixture_func( except StopIteration: raise ValueError(f"{request.fixturename} did not yield a value") from None finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator) - request.addfinalizer(finalizer) - else: + request.addfinalizer(finalizer) + else: fixturefunc = cast(Callable[..., _FixtureValue], fixturefunc) fixture_result = fixturefunc(**kwargs) return fixture_result - - + + def _teardown_yield_fixture(fixturefunc, it) -> None: """Execute the teardown of a fixture function by advancing the iterator after the yield and ensure the iteration ends (if not it means there is more than one yield in the function).""" - try: - next(it) - except StopIteration: - pass - else: + try: + next(it) + except StopIteration: + pass + else: fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'") - - + + def _eval_scope_callable( scope_callable: "Callable[[str, Config], _Scope]", fixture_name: str, @@ -973,9 +973,9 @@ def _eval_scope_callable( @final class FixtureDef(Generic[_FixtureValue]): """A container for a factory definition.""" - - def __init__( - self, + + def __init__( + self, fixturemanager: "FixtureManager", baseid: Optional[str], argname: str, @@ -990,41 +990,41 @@ class FixtureDef(Generic[_FixtureValue]): ] ] = None, ) -> None: - self._fixturemanager = fixturemanager - self.baseid = baseid or "" - self.has_location = baseid is not None - self.func = func - self.argname = argname + self._fixturemanager = fixturemanager + self.baseid = baseid or "" + self.has_location = baseid is not None + self.func = func + self.argname = argname if callable(scope): scope_ = _eval_scope_callable(scope, argname, fixturemanager.config) else: scope_ = scope - self.scopenum = scope2index( + self.scopenum = scope2index( # TODO: Check if the `or` here is really necessary. scope_ or "function", # type: ignore[unreachable] descr=f"Fixture '{func.__name__}'", - where=baseid, - ) + where=baseid, + ) self.scope = scope_ self.params: Optional[Sequence[object]] = params self.argnames: Tuple[str, ...] = getfuncargnames( func, name=argname, is_method=unittest ) - self.unittest = unittest - self.ids = ids + self.unittest = unittest + self.ids = ids self.cached_result: Optional[_FixtureCachedResult[_FixtureValue]] = None self._finalizers: List[Callable[[], object]] = [] - + def addfinalizer(self, finalizer: Callable[[], object]) -> None: - self._finalizers.append(finalizer) - + self._finalizers.append(finalizer) + def finish(self, request: SubRequest) -> None: exc = None - try: - while self._finalizers: - try: - func = self._finalizers.pop() - func() + try: + while self._finalizers: + try: + func = self._finalizers.pop() + func() except BaseException as e: # XXX Only first exception will be seen by user, # ideally all should be reported. @@ -1032,25 +1032,25 @@ class FixtureDef(Generic[_FixtureValue]): exc = e if exc: raise exc - finally: - hook = self._fixturemanager.session.gethookproxy(request.node.fspath) - hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) + finally: + hook = self._fixturemanager.session.gethookproxy(request.node.fspath) + hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) # Even if finalization fails, we invalidate the cached fixture # value and remove all finalizers because they may be bound methods # which will keep instances alive. self.cached_result = None - self._finalizers = [] - + self._finalizers = [] + def execute(self, request: SubRequest) -> _FixtureValue: # Get required arguments and register our own finish() # with their finalization. - for argname in self.argnames: - fixturedef = request._get_active_fixturedef(argname) - if argname != "request": + for argname in self.argnames: + fixturedef = request._get_active_fixturedef(argname) + if argname != "request": # PseudoFixtureDef is only for "request". assert isinstance(fixturedef, FixtureDef) - fixturedef.addfinalizer(functools.partial(self.finish, request=request)) - + fixturedef.addfinalizer(functools.partial(self.finish, request=request)) + my_cache_key = self.cache_key(request) if self.cached_result is not None: # note: comparison with `==` can fail (or be expensive) for e.g. @@ -1060,79 +1060,79 @@ class FixtureDef(Generic[_FixtureValue]): if self.cached_result[2] is not None: _, val, tb = self.cached_result[2] raise val.with_traceback(tb) - else: + else: result = self.cached_result[0] - return result + return result # We have a previous but differently parametrized fixture instance # so we need to tear it down before creating a new one. - self.finish(request) + self.finish(request) assert self.cached_result is None - - hook = self._fixturemanager.session.gethookproxy(request.node.fspath) + + hook = self._fixturemanager.session.gethookproxy(request.node.fspath) result = hook.pytest_fixture_setup(fixturedef=self, request=request) return result - + def cache_key(self, request: SubRequest) -> object: return request.param_index if not hasattr(request, "param") else request.param def __repr__(self) -> str: return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format( self.argname, self.scope, self.baseid - ) - - + ) + + def resolve_fixture_function( fixturedef: FixtureDef[_FixtureValue], request: FixtureRequest ) -> "_FixtureFunc[_FixtureValue]": """Get the actual callable that can be called to obtain the fixture value, dealing with unittest-specific instances and bound methods.""" - fixturefunc = fixturedef.func - if fixturedef.unittest: - if request.instance is not None: + fixturefunc = fixturedef.func + if fixturedef.unittest: + if request.instance is not None: # Bind the unbound method to the TestCase instance. fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] - else: + else: # The fixture function needs to be bound to the actual - # request.instance so that code working with "fixturedef" behaves - # as expected. - if request.instance is not None: + # request.instance so that code working with "fixturedef" behaves + # as expected. + if request.instance is not None: # 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. if hasattr(fixturefunc, "__self__") and not isinstance( request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] ): return fixturefunc - fixturefunc = getimfunc(fixturedef.func) - if fixturefunc != fixturedef.func: + fixturefunc = getimfunc(fixturedef.func) + if fixturefunc != fixturedef.func: fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr] - return fixturefunc - - + return fixturefunc + + def pytest_fixture_setup( fixturedef: FixtureDef[_FixtureValue], request: SubRequest ) -> _FixtureValue: """Execution of fixture setup.""" - kwargs = {} - for argname in fixturedef.argnames: - fixdef = request._get_active_fixturedef(argname) + kwargs = {} + for argname in fixturedef.argnames: + fixdef = request._get_active_fixturedef(argname) assert fixdef.cached_result is not None - result, arg_cache_key, exc = fixdef.cached_result - request._check_scope(argname, request.scope, fixdef.scope) - kwargs[argname] = result - - fixturefunc = resolve_fixture_function(fixturedef, request) + result, arg_cache_key, exc = fixdef.cached_result + request._check_scope(argname, request.scope, fixdef.scope) + kwargs[argname] = result + + fixturefunc = resolve_fixture_function(fixturedef, request) my_cache_key = fixturedef.cache_key(request) - try: - result = call_fixture_func(fixturefunc, request, kwargs) - except TEST_OUTCOME: + try: + result = call_fixture_func(fixturefunc, request, kwargs) + except TEST_OUTCOME: exc_info = sys.exc_info() assert exc_info[0] is not None fixturedef.cached_result = (None, my_cache_key, exc_info) - raise - fixturedef.cached_result = (result, my_cache_key, None) - return result - - + raise + fixturedef.cached_result = (result, my_cache_key, None) + return result + + def _ensure_immutable_ids( ids: Optional[ Union[ @@ -1146,13 +1146,13 @@ def _ensure_immutable_ids( Callable[[Any], Optional[object]], ] ]: - if ids is None: + if ids is None: return None - if callable(ids): - return ids - return tuple(ids) - - + if callable(ids): + return ids + return tuple(ids) + + def _params_converter( params: Optional[Iterable[object]], ) -> Optional[Tuple[object, ...]]: @@ -1170,20 +1170,20 @@ def wrap_function_to_error_out_if_called_directly( "See https://docs.pytest.org/en/stable/fixture.html for more information about fixtures, and\n" "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code." ).format(name=fixture_marker.name or function.__name__) - + @functools.wraps(function) def result(*args, **kwargs): fail(message, pytrace=False) - + # 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). result.__pytest_wrapped__ = _PytestWrapper(function) # type: ignore[attr-defined] - + return cast(_FixtureFunction, result) - - + + @final -@attr.s(frozen=True) +@attr.s(frozen=True) class FixtureFunctionMarker: scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]") params = attr.ib(type=Optional[Tuple[object, ...]], converter=_params_converter) @@ -1197,20 +1197,20 @@ class FixtureFunctionMarker: converter=_ensure_immutable_ids, ) name = attr.ib(type=Optional[str], default=None) - + def __call__(self, function: _FixtureFunction) -> _FixtureFunction: if inspect.isclass(function): - raise ValueError("class fixtures not supported (maybe in the future)") - - if getattr(function, "_pytestfixturefunction", False): - raise ValueError( - "fixture is being applied more than once to the same function" - ) - + raise ValueError("class fixtures not supported (maybe in the future)") + + if getattr(function, "_pytestfixturefunction", False): + raise ValueError( + "fixture is being applied more than once to the same function" + ) + function = wrap_function_to_error_out_if_called_directly(function, self) - - name = self.name or function.__name__ - if name == "request": + + name = self.name or function.__name__ + if name == "request": location = getlocation(function) fail( "'request' is a reserved word for fixtures, use another name:\n {}".format( @@ -1221,9 +1221,9 @@ class FixtureFunctionMarker: # Type ignored because https://github.com/python/mypy/issues/2087. function._pytestfixturefunction = self # type: ignore[attr-defined] - return function - - + return function + + @overload def fixture( fixture_function: _FixtureFunction, @@ -1274,48 +1274,48 @@ def fixture( ] = None, name: Optional[str] = None, ) -> Union[FixtureFunctionMarker, _FixtureFunction]: - """Decorator to mark a fixture factory function. - - This decorator can be used, with or without parameters, to define a - fixture function. - - The name of the fixture function can later be referenced to cause its + """Decorator to mark a fixture factory function. + + This decorator can be used, with or without parameters, to define a + fixture function. + + The name of the fixture function can later be referenced to cause its invocation ahead of running tests: test modules or classes can use the ``pytest.mark.usefixtures(fixturename)`` marker. - + Test functions can directly use fixture names as input arguments in which case the fixture instance returned from the fixture function will be injected. - + Fixtures can provide their values to test functions using ``return`` or ``yield`` statements. When using ``yield`` the code block after the ``yield`` statement is executed as teardown code regardless of the test outcome, and must yield exactly once. - + :param scope: The scope for which this fixture is shared; one of ``"function"`` (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``. - + 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. - + See :ref:`dynamic scope` in the docs for more information. :param params: An optional list of parameters which will cause multiple invocations of the fixture function and all of the tests using it. The current parameter is available in ``request.param``. - + :param autouse: If True, the fixture func is activated for all tests that can see it. If False (the default), an explicit reference is needed to activate the fixture. - + :param ids: List of string ids each corresponding to the params so that they are part of the test id. If no ids are provided they will be generated automatically from the params. - + :param name: The name of the fixture. This defaults to the name of the decorated function. If a fixture is used in the same module in which it is @@ -1323,7 +1323,7 @@ def fixture( function arg that requests the fixture; one way to resolve this is to name the decorated function ``fixture_<fixturename>`` and then use ``@pytest.fixture(name='<fixturename>')``. - """ + """ fixture_marker = FixtureFunctionMarker( scope=scope, params=params, autouse=autouse, ids=ids, name=name, ) @@ -1333,8 +1333,8 @@ def fixture( return fixture_marker(fixture_function) return fixture_marker - - + + def yield_fixture( fixture_function=None, *args, @@ -1345,10 +1345,10 @@ def yield_fixture( name=None, ): """(Return a) decorator to mark a yield-fixture factory function. - - .. deprecated:: 3.0 - Use :py:func:`pytest.fixture` directly instead. - """ + + .. deprecated:: 3.0 + Use :py:func:`pytest.fixture` directly instead. + """ warnings.warn(YIELD_FIXTURE, stacklevel=2) return fixture( fixture_function, @@ -1359,22 +1359,22 @@ def yield_fixture( ids=ids, name=name, ) - - -@fixture(scope="session") + + +@fixture(scope="session") def pytestconfig(request: FixtureRequest) -> Config: - """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. - - Example:: - - def test_foo(pytestconfig): + """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. + + Example:: + + def test_foo(pytestconfig): if pytestconfig.getoption("verbose") > 0: - ... - - """ - return request.config - - + ... + + """ + return request.config + + def pytest_addoption(parser: Parser) -> None: parser.addini( "usefixtures", @@ -1386,40 +1386,40 @@ def pytest_addoption(parser: Parser) -> None: class FixtureManager: """pytest fixture definitions and information is stored and managed - from this class. - - During collection fm.parsefactories() is called multiple times to parse - fixture function definitions into FixtureDef objects and internal - data structures. - - During collection of test functions, metafunc-mechanics instantiate - a FuncFixtureInfo object which is cached per node/func-name. - This FuncFixtureInfo object is later retrieved by Function nodes - which themselves offer a fixturenames attribute. - - The FuncFixtureInfo object holds information about fixtures and FixtureDefs + from this class. + + During collection fm.parsefactories() is called multiple times to parse + fixture function definitions into FixtureDef objects and internal + data structures. + + During collection of test functions, metafunc-mechanics instantiate + a FuncFixtureInfo object which is cached per node/func-name. + This FuncFixtureInfo object is later retrieved by Function nodes + which themselves offer a fixturenames attribute. + + The FuncFixtureInfo object holds information about fixtures and FixtureDefs relevant for a particular function. An initial list of fixtures is - assembled like this: - - - ini-defined usefixtures - - autouse-marked fixtures along the collection chain up from the function - - usefixtures markers at module/class/function level - - test function funcargs - - Subsequently the funcfixtureinfo.fixturenames attribute is computed - as the closure of the fixtures needed to setup the initial fixtures, + assembled like this: + + - ini-defined usefixtures + - autouse-marked fixtures along the collection chain up from the function + - usefixtures markers at module/class/function level + - test function funcargs + + Subsequently the funcfixtureinfo.fixturenames attribute is computed + as the closure of the fixtures needed to setup the initial fixtures, i.e. fixtures needed by fixture functions themselves are appended - to the fixturenames list. - - Upon the test-setup phases all fixturenames are instantiated, retrieved - by a lookup of their FuncFixtureInfo. - """ - - FixtureLookupError = FixtureLookupError - FixtureLookupErrorRepr = FixtureLookupErrorRepr - + to the fixturenames list. + + Upon the test-setup phases all fixturenames are instantiated, retrieved + by a lookup of their FuncFixtureInfo. + """ + + FixtureLookupError = FixtureLookupError + FixtureLookupErrorRepr = FixtureLookupErrorRepr + def __init__(self, session: "Session") -> None: - self.session = session + self.session = session self.config: Config = session.config self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {} self._holderobjseen: Set[object] = set() @@ -1427,8 +1427,8 @@ class FixtureManager: self._nodeid_autousenames: Dict[str, List[str]] = { "": self.config.getini("usefixtures"), } - session.config.pluginmanager.register(self, "funcmanage") - + session.config.pluginmanager.register(self, "funcmanage") + def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]: """Return all direct parametrization arguments of a node, so we don't mistake them for fixtures. @@ -1451,30 +1451,30 @@ class FixtureManager: def getfixtureinfo( self, node: nodes.Node, func, cls, funcargs: bool = True ) -> FuncFixtureInfo: - if funcargs and not getattr(node, "nofuncargs", False): + if funcargs and not getattr(node, "nofuncargs", False): argnames = getfuncargnames(func, name=node.name, cls=cls) - else: - argnames = () + else: + argnames = () usefixtures = tuple( arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args - ) + ) initialnames = usefixtures + argnames - fm = node.session._fixturemanager - initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( + fm = node.session._fixturemanager + initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( initialnames, node, ignore_args=self._get_direct_parametrize_args(node) - ) - return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) - + ) + return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: - nodeid = None - try: + nodeid = None + try: p = absolutepath(plugin.__file__) # type: ignore[attr-defined] - except AttributeError: - pass - else: + except AttributeError: + pass + else: # 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). if p.name.startswith("conftest.py"): try: @@ -1485,16 +1485,16 @@ class FixtureManager: nodeid = "" if os.sep != nodes.SEP: nodeid = nodeid.replace(os.sep, nodes.SEP) - - self.parsefactories(plugin, nodeid) - + + self.parsefactories(plugin, nodeid) + def _getautousenames(self, nodeid: str) -> Iterator[str]: """Return the names of autouse fixtures applicable to nodeid.""" for parentnodeid in nodes.iterparentnodeids(nodeid): basenames = self._nodeid_autousenames.get(parentnodeid) if basenames: yield from basenames - + def getfixtureclosure( self, fixturenames: Tuple[str, ...], @@ -1502,52 +1502,52 @@ class FixtureManager: ignore_args: Sequence[str] = (), ) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given - # fixturenames as the initial set. As we have to visit all - # factory definitions anyway, we also return an arg2fixturedefs - # mapping so that the caller can reuse it and does not have - # to re-discover fixturedefs again for each fixturename + # fixturenames as the initial set. As we have to visit all + # factory definitions anyway, we also return an arg2fixturedefs + # mapping so that the caller can reuse it and does not have + # to re-discover fixturedefs again for each fixturename # (discovering matching fixtures for a given name/node is expensive). - - parentid = parentnode.nodeid + + parentid = parentnode.nodeid fixturenames_closure = list(self._getautousenames(parentid)) - + def merge(otherlist: Iterable[str]) -> None: - for arg in otherlist: - if arg not in fixturenames_closure: - fixturenames_closure.append(arg) - - merge(fixturenames) - + for arg in otherlist: + if arg not in fixturenames_closure: + fixturenames_closure.append(arg) + + merge(fixturenames) + # At this point, fixturenames_closure contains what we call "initialnames", - # which is a set of fixturenames the function immediately requests. We - # need to return it as well, so save this. - initialnames = tuple(fixturenames_closure) - + # which is a set of fixturenames the function immediately requests. We + # need to return it as well, so save this. + initialnames = tuple(fixturenames_closure) + arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} - lastlen = -1 - while lastlen != len(fixturenames_closure): - lastlen = len(fixturenames_closure) - for argname in fixturenames_closure: + lastlen = -1 + while lastlen != len(fixturenames_closure): + lastlen = len(fixturenames_closure) + for argname in fixturenames_closure: if argname in ignore_args: continue - if argname in arg2fixturedefs: - continue - fixturedefs = self.getfixturedefs(argname, parentid) - if fixturedefs: - arg2fixturedefs[argname] = fixturedefs - merge(fixturedefs[-1].argnames) - + if argname in arg2fixturedefs: + continue + fixturedefs = self.getfixturedefs(argname, parentid) + if fixturedefs: + arg2fixturedefs[argname] = fixturedefs + merge(fixturedefs[-1].argnames) + def sort_by_scope(arg_name: str) -> int: - try: - fixturedefs = arg2fixturedefs[arg_name] - except KeyError: - return scopes.index("function") - else: - return fixturedefs[-1].scopenum - - fixturenames_closure.sort(key=sort_by_scope) - return initialnames, fixturenames_closure, arg2fixturedefs - + try: + fixturedefs = arg2fixturedefs[arg_name] + except KeyError: + return scopes.index("function") + else: + return fixturedefs[-1].scopenum + + fixturenames_closure.sort(key=sort_by_scope) + return initialnames, fixturenames_closure, arg2fixturedefs + def pytest_generate_tests(self, metafunc: "Metafunc") -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" @@ -1555,7 +1555,7 @@ class FixtureManager: args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs) return args - for argname in metafunc.fixturenames: + for argname in metafunc.fixturenames: # Get the FixtureDefs for the argname. fixture_defs = metafunc._arg2fixturedefs.get(argname) if not fixture_defs: @@ -1577,7 +1577,7 @@ class FixtureManager: # in case the super fixture is parametrized (#1953). for fixturedef in reversed(fixture_defs): # Fixture is parametrized, apply it and stop. - if fixturedef.params is not None: + if fixturedef.params is not None: metafunc.parametrize( argname, fixturedef.params, @@ -1590,36 +1590,36 @@ class FixtureManager: # Not requesting the overridden super fixture, stop. if argname not in fixturedef.argnames: break - + # Try next super fixture, if any. def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None: # Separate parametrized setups. - items[:] = reorder_items(items) - + items[:] = reorder_items(items) + def parsefactories( self, node_or_obj, nodeid=NOTSET, unittest: bool = False ) -> None: - if nodeid is not NOTSET: - holderobj = node_or_obj - else: - holderobj = node_or_obj.obj - nodeid = node_or_obj.nodeid - if holderobj in self._holderobjseen: - return - - self._holderobjseen.add(holderobj) - autousenames = [] - for name in dir(holderobj): - # The attribute can be an arbitrary descriptor, so the attribute - # access below can raise. safe_getatt() ignores such exceptions. - obj = safe_getattr(holderobj, name, None) - marker = getfixturemarker(obj) + if nodeid is not NOTSET: + holderobj = node_or_obj + else: + holderobj = node_or_obj.obj + nodeid = node_or_obj.nodeid + if holderobj in self._holderobjseen: + return + + self._holderobjseen.add(holderobj) + autousenames = [] + for name in dir(holderobj): + # The attribute can be an arbitrary descriptor, so the attribute + # access below can raise. safe_getatt() ignores such exceptions. + obj = safe_getattr(holderobj, name, None) + marker = getfixturemarker(obj) if not isinstance(marker, FixtureFunctionMarker): # Magic globals with __getattr__ might have got us a wrong # fixture attribute. - continue - + continue + if marker.name: name = marker.name @@ -1628,53 +1628,53 @@ class FixtureManager: # order to not emit the warning when pytest itself calls the # fixture function. obj = get_real_method(obj, holderobj) - - fixture_def = FixtureDef( + + fixture_def = FixtureDef( fixturemanager=self, baseid=nodeid, argname=name, func=obj, scope=marker.scope, params=marker.params, - unittest=unittest, - ids=marker.ids, - ) - - faclist = self._arg2fixturedefs.setdefault(name, []) - if fixture_def.has_location: - faclist.append(fixture_def) - else: - # fixturedefs with no location are at the front - # so this inserts the current fixturedef after the - # existing fixturedefs from external plugins but - # before the fixturedefs provided in conftests. - i = len([f for f in faclist if not f.has_location]) - faclist.insert(i, fixture_def) - if marker.autouse: - autousenames.append(name) - - if autousenames: + unittest=unittest, + ids=marker.ids, + ) + + faclist = self._arg2fixturedefs.setdefault(name, []) + if fixture_def.has_location: + faclist.append(fixture_def) + else: + # fixturedefs with no location are at the front + # so this inserts the current fixturedef after the + # existing fixturedefs from external plugins but + # before the fixturedefs provided in conftests. + i = len([f for f in faclist if not f.has_location]) + faclist.insert(i, fixture_def) + if marker.autouse: + autousenames.append(name) + + if autousenames: self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames) - + def getfixturedefs( self, argname: str, nodeid: str ) -> Optional[Sequence[FixtureDef[Any]]]: """Get a list of fixtures which are applicable to the given node id. - + :param str argname: Name of the fixture to search for. :param str nodeid: Full node id of the requesting test. :rtype: Sequence[FixtureDef] - """ - try: - fixturedefs = self._arg2fixturedefs[argname] - except KeyError: - return None - return tuple(self._matchfactories(fixturedefs, nodeid)) - + """ + try: + fixturedefs = self._arg2fixturedefs[argname] + except KeyError: + return None + return tuple(self._matchfactories(fixturedefs, nodeid)) + def _matchfactories( self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str ) -> Iterator[FixtureDef[Any]]: parentnodeids = set(nodes.iterparentnodeids(nodeid)) - for fixturedef in fixturedefs: + for fixturedef in fixturedefs: if fixturedef.baseid in parentnodeids: - yield fixturedef + yield fixturedef diff --git a/contrib/python/pytest/py3/_pytest/freeze_support.py b/contrib/python/pytest/py3/_pytest/freeze_support.py index 86df297302..8b93ed5f7f 100644 --- a/contrib/python/pytest/py3/_pytest/freeze_support.py +++ b/contrib/python/pytest/py3/_pytest/freeze_support.py @@ -4,42 +4,42 @@ import types from typing import Iterator from typing import List from typing import Union - - + + def freeze_includes() -> List[str]: """Return a list of module names used by pytest that should be included by cx_freeze.""" - import py - import _pytest - - result = list(_iter_all_modules(py)) - result += list(_iter_all_modules(_pytest)) - return result - - + import py + import _pytest + + result = list(_iter_all_modules(py)) + result += list(_iter_all_modules(_pytest)) + return result + + def _iter_all_modules( package: Union[str, types.ModuleType], prefix: str = "", ) -> Iterator[str]: """Iterate over the names of all modules that can be found in the given - package, recursively. + package, recursively. >>> import _pytest >>> list(_iter_all_modules(_pytest)) ['_pytest._argcomplete', '_pytest._code.code', ...] - """ - import os - import pkgutil - + """ + import os + import pkgutil + if isinstance(package, str): path = package - else: + else: # Type ignored because typeshed doesn't define ModuleType.__path__ # (only defined on packages). package_path = package.__path__ # type: ignore[attr-defined] path, prefix = package_path[0], package.__name__ + "." - for _, name, is_package in pkgutil.iter_modules([path]): - if is_package: - for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."): - yield prefix + m - else: - yield prefix + name + for _, name, is_package in pkgutil.iter_modules([path]): + if is_package: + for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."): + yield prefix + m + else: + yield prefix + name diff --git a/contrib/python/pytest/py3/_pytest/helpconfig.py b/contrib/python/pytest/py3/_pytest/helpconfig.py index a417d05374..4384d07b26 100644 --- a/contrib/python/pytest/py3/_pytest/helpconfig.py +++ b/contrib/python/pytest/py3/_pytest/helpconfig.py @@ -1,130 +1,130 @@ """Version info, help messages, tracing configuration.""" -import os -import sys -from argparse import Action +import os +import sys +from argparse import Action from typing import List from typing import Optional from typing import Union - -import py - -import pytest + +import py + +import pytest from _pytest.config import Config from _pytest.config import ExitCode -from _pytest.config import PrintHelp +from _pytest.config import PrintHelp from _pytest.config.argparsing import Parser - - -class HelpAction(Action): + + +class HelpAction(Action): """An argparse Action that will raise an exception in order to skip the rest of the argument parsing when --help is passed. - This prevents argparse from quitting due to missing required arguments - when any are defined, for example by ``pytest_addoption``. - This is similar to the way that the builtin argparse --help option is - implemented by raising SystemExit. - """ - - def __init__(self, option_strings, dest=None, default=False, help=None): + This prevents argparse from quitting due to missing required arguments + when any are defined, for example by ``pytest_addoption``. + This is similar to the way that the builtin argparse --help option is + implemented by raising SystemExit. + """ + + def __init__(self, option_strings, dest=None, default=False, help=None): super().__init__( - option_strings=option_strings, - dest=dest, - const=True, - default=default, - nargs=0, - help=help, - ) - - def __call__(self, parser, namespace, values, option_string=None): - setattr(namespace, self.dest, self.const) - + option_strings=option_strings, + dest=dest, + const=True, + default=default, + nargs=0, + help=help, + ) + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, self.const) + # We should only skip the rest of the parsing after preparse is done. - if getattr(parser._parser, "after_preparse", False): - raise PrintHelp - - + if getattr(parser._parser, "after_preparse", False): + raise PrintHelp + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("debugconfig") - group.addoption( - "--version", + group = parser.getgroup("debugconfig") + group.addoption( + "--version", "-V", action="count", default=0, dest="version", help="display pytest version and information about plugins." "When given twice, also display information about plugins.", - ) - group._addoption( - "-h", - "--help", - action=HelpAction, - dest="help", - help="show help message and configuration info", - ) - group._addoption( - "-p", - action="append", - dest="plugins", - default=[], - metavar="name", + ) + group._addoption( + "-h", + "--help", + action=HelpAction, + dest="help", + help="show help message and configuration info", + ) + group._addoption( + "-p", + action="append", + dest="plugins", + default=[], + metavar="name", help="early-load given plugin module name or entry point (multi-allowed).\n" - "To avoid loading of plugins, use the `no:` prefix, e.g. " - "`no:doctest`.", - ) - group.addoption( - "--traceconfig", - "--trace-config", - action="store_true", - default=False, - help="trace considerations of conftest.py files.", + "To avoid loading of plugins, use the `no:` prefix, e.g. " + "`no:doctest`.", + ) + group.addoption( + "--traceconfig", + "--trace-config", + action="store_true", + default=False, + help="trace considerations of conftest.py files.", + ) + group.addoption( + "--debug", + action="store_true", + dest="debug", + default=False, + help="store internal tracing debug information in 'pytestdebug.log'.", + ) + group._addoption( + "-o", + "--override-ini", + dest="override_ini", + action="append", + help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.', ) - group.addoption( - "--debug", - action="store_true", - dest="debug", - default=False, - help="store internal tracing debug information in 'pytestdebug.log'.", - ) - group._addoption( - "-o", - "--override-ini", - dest="override_ini", - action="append", - help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.', - ) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_cmdline_parse(): - outcome = yield + + +@pytest.hookimpl(hookwrapper=True) +def pytest_cmdline_parse(): + outcome = yield config: Config = outcome.get_result() - if config.option.debug: - path = os.path.abspath("pytestdebug.log") - debugfile = open(path, "w") - debugfile.write( - "versions pytest-%s, py-%s, " - "python-%s\ncwd=%s\nargs=%s\n\n" - % ( - pytest.__version__, - py.__version__, - ".".join(map(str, sys.version_info)), - os.getcwd(), + if config.option.debug: + path = os.path.abspath("pytestdebug.log") + debugfile = open(path, "w") + debugfile.write( + "versions pytest-%s, py-%s, " + "python-%s\ncwd=%s\nargs=%s\n\n" + % ( + pytest.__version__, + py.__version__, + ".".join(map(str, sys.version_info)), + os.getcwd(), config.invocation_params.args, - ) - ) - config.trace.root.setwriter(debugfile.write) - undo_tracing = config.pluginmanager.enable_tracing() - sys.stderr.write("writing pytestdebug information to %s\n" % path) - + ) + ) + config.trace.root.setwriter(debugfile.write) + undo_tracing = config.pluginmanager.enable_tracing() + sys.stderr.write("writing pytestdebug information to %s\n" % path) + def unset_tracing() -> None: - debugfile.close() - sys.stderr.write("wrote pytestdebug information to %s\n" % debugfile.name) - config.trace.root.setwriter(None) - undo_tracing() - - config.add_cleanup(unset_tracing) - - + debugfile.close() + sys.stderr.write("wrote pytestdebug information to %s\n" % debugfile.name) + config.trace.root.setwriter(None) + undo_tracing() + + config.add_cleanup(unset_tracing) + + def showversion(config: Config) -> None: if config.option.version > 1: sys.stderr.write( @@ -143,34 +143,34 @@ def showversion(config: Config) -> None: def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: if config.option.version > 0: showversion(config) - return 0 - elif config.option.help: - config._do_configure() - showhelp(config) - config._ensure_unconfigure() - return 0 + return 0 + elif config.option.help: + config._do_configure() + showhelp(config) + config._ensure_unconfigure() + return 0 return None - - + + def showhelp(config: Config) -> None: import textwrap - reporter = config.pluginmanager.get_plugin("terminalreporter") - tw = reporter._tw - tw.write(config._parser.optparser.format_help()) - tw.line() - tw.line( - "[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:" - ) - tw.line() - + reporter = config.pluginmanager.get_plugin("terminalreporter") + tw = reporter._tw + tw.write(config._parser.optparser.format_help()) + tw.line() + tw.line( + "[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:" + ) + tw.line() + columns = tw.fullwidth # costly call indent_len = 24 # based on argparse's max_help_position=24 indent = " " * indent_len - for name in config._parser._ininames: - help, type, default = config._parser._inidict[name] - if type is None: - type = "string" + for name in config._parser._ininames: + help, type, default = config._parser._inidict[name] + if type is None: + type = "string" if help is None: raise TypeError(f"help argument cannot be None for {name}") spec = f"{name} ({type}):" @@ -186,7 +186,7 @@ def showhelp(config: Config) -> None: subsequent_indent=indent, break_on_hyphens=False, ) - + for line in helplines: tw.line(line) else: @@ -199,63 +199,63 @@ def showhelp(config: Config) -> None: for line in wrapped[1:]: tw.line(indent + line) - tw.line() - tw.line("environment variables:") - vars = [ - ("PYTEST_ADDOPTS", "extra command line options"), - ("PYTEST_PLUGINS", "comma-separated plugins to load during startup"), - ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "set to disable plugin auto-loading"), - ("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"), - ] - for name, help in vars: + tw.line() + tw.line("environment variables:") + vars = [ + ("PYTEST_ADDOPTS", "extra command line options"), + ("PYTEST_PLUGINS", "comma-separated plugins to load during startup"), + ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "set to disable plugin auto-loading"), + ("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"), + ] + for name, help in vars: tw.line(f" {name:<24} {help}") - tw.line() - tw.line() - - tw.line("to see available markers type: pytest --markers") - tw.line("to see available fixtures type: pytest --fixtures") - tw.line( - "(shown according to specified file_or_dir or current dir " - "if not specified; fixtures with leading '_' are only shown " - "with the '-v' option" - ) - - for warningreport in reporter.stats.get("warnings", []): - tw.line("warning : " + warningreport.message, red=True) - return - - -conftest_options = [("pytest_plugins", "list of plugin names to load")] - - + tw.line() + tw.line() + + tw.line("to see available markers type: pytest --markers") + tw.line("to see available fixtures type: pytest --fixtures") + tw.line( + "(shown according to specified file_or_dir or current dir " + "if not specified; fixtures with leading '_' are only shown " + "with the '-v' option" + ) + + for warningreport in reporter.stats.get("warnings", []): + tw.line("warning : " + warningreport.message, red=True) + return + + +conftest_options = [("pytest_plugins", "list of plugin names to load")] + + def getpluginversioninfo(config: Config) -> List[str]: - lines = [] - plugininfo = config.pluginmanager.list_plugin_distinfo() - if plugininfo: - lines.append("setuptools registered plugins:") - for plugin, dist in plugininfo: - loc = getattr(plugin, "__file__", repr(plugin)) + lines = [] + plugininfo = config.pluginmanager.list_plugin_distinfo() + if plugininfo: + lines.append("setuptools registered plugins:") + for plugin, dist in plugininfo: + loc = getattr(plugin, "__file__", repr(plugin)) content = f"{dist.project_name}-{dist.version} at {loc}" - lines.append(" " + content) - return lines - - + lines.append(" " + content) + return lines + + def pytest_report_header(config: Config) -> List[str]: - lines = [] - if config.option.debug or config.option.traceconfig: + lines = [] + if config.option.debug or config.option.traceconfig: lines.append(f"using: pytest-{pytest.__version__} pylib-{py.__version__}") - - verinfo = getpluginversioninfo(config) - if verinfo: - lines.extend(verinfo) - - if config.option.traceconfig: - lines.append("active plugins:") - items = config.pluginmanager.list_name_plugin() - for name, plugin in items: - if hasattr(plugin, "__file__"): - r = plugin.__file__ - else: - r = repr(plugin) + + verinfo = getpluginversioninfo(config) + if verinfo: + lines.extend(verinfo) + + if config.option.traceconfig: + lines.append("active plugins:") + items = config.pluginmanager.list_name_plugin() + for name, plugin in items: + if hasattr(plugin, "__file__"): + r = plugin.__file__ + else: + r = repr(plugin) lines.append(f" {name:<20}: {r}") - return lines + return lines diff --git a/contrib/python/pytest/py3/_pytest/hookspec.py b/contrib/python/pytest/py3/_pytest/hookspec.py index ef571bb0eb..e499b742c7 100644 --- a/contrib/python/pytest/py3/_pytest/hookspec.py +++ b/contrib/python/pytest/py3/_pytest/hookspec.py @@ -11,10 +11,10 @@ from typing import TYPE_CHECKING from typing import Union import py.path -from pluggy import HookspecMarker - +from pluggy import HookspecMarker + from _pytest.deprecated import WARNING_CAPTURED_HOOK - + if TYPE_CHECKING: import pdb import warnings @@ -43,174 +43,174 @@ if TYPE_CHECKING: from _pytest.terminal import TerminalReporter -hookspec = HookspecMarker("pytest") - -# ------------------------------------------------------------------------- -# Initialization hooks called for every plugin -# ------------------------------------------------------------------------- - - -@hookspec(historic=True) +hookspec = HookspecMarker("pytest") + +# ------------------------------------------------------------------------- +# Initialization hooks called for every plugin +# ------------------------------------------------------------------------- + + +@hookspec(historic=True) def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: """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. - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - """ - - -@hookspec(historic=True) + + .. note:: + This hook is incompatible with ``hookwrapper=True``. + """ + + +@hookspec(historic=True) def pytest_plugin_registered( plugin: "_PluggyPlugin", manager: "PytestPluginManager" ) -> None: """A new pytest plugin got registered. - + :param plugin: The plugin module or instance. :param _pytest.config.PytestPluginManager manager: pytest plugin manager. - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - """ - - -@hookspec(historic=True) + + .. note:: + This hook is incompatible with ``hookwrapper=True``. + """ + + +@hookspec(historic=True) def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None: """Register argparse-style options and ini-style config values, - called once at the beginning of a test run. - - .. note:: - - This function should be implemented only in plugins or ``conftest.py`` - files situated at the tests root directory due to how pytest - :ref:`discovers plugins during startup <pluginorder>`. - + called once at the beginning of a test run. + + .. note:: + + This function should be implemented only in plugins or ``conftest.py`` + files situated at the tests root directory due to how pytest + :ref:`discovers plugins during startup <pluginorder>`. + :param _pytest.config.argparsing.Parser parser: To add command line options, call :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>`. - + :param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager, which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s and allow one plugin to call another plugin's hooks to change how command line options are added. - Options can later be accessed through the - :py:class:`config <_pytest.config.Config>` object, respectively: - - - :py:func:`config.getoption(name) <_pytest.config.Config.getoption>` to - retrieve the value of a command line option. - - - :py:func:`config.getini(name) <_pytest.config.Config.getini>` to retrieve - a value read from an ini-style file. - - The config object is passed around on many internal objects via the ``.config`` - attribute or can be retrieved as the ``pytestconfig`` fixture. - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - """ - - -@hookspec(historic=True) + Options can later be accessed through the + :py:class:`config <_pytest.config.Config>` object, respectively: + + - :py:func:`config.getoption(name) <_pytest.config.Config.getoption>` to + retrieve the value of a command line option. + + - :py:func:`config.getini(name) <_pytest.config.Config.getini>` to retrieve + a value read from an ini-style file. + + The config object is passed around on many internal objects via the ``.config`` + attribute or can be retrieved as the ``pytestconfig`` fixture. + + .. note:: + This hook is incompatible with ``hookwrapper=True``. + """ + + +@hookspec(historic=True) def pytest_configure(config: "Config") -> None: """Allow plugins and conftest files to perform initial configuration. - - This hook is called for every plugin and initial conftest file - after command line options have been parsed. - - After that, the hook is called for other conftest files as they are - imported. - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - + + This hook is called for every plugin and initial conftest file + after command line options have been parsed. + + After that, the hook is called for other conftest files as they are + imported. + + .. note:: + This hook is incompatible with ``hookwrapper=True``. + :param _pytest.config.Config config: The pytest config object. - """ - - -# ------------------------------------------------------------------------- -# Bootstrapping hooks called for plugins registered early enough: -# internal and 3rd party plugins. -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) + """ + + +# ------------------------------------------------------------------------- +# Bootstrapping hooks called for plugins registered early enough: +# internal and 3rd party plugins. +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) def pytest_cmdline_parse( pluginmanager: "PytestPluginManager", args: List[str] ) -> Optional["Config"]: """Return an initialized config object, parsing the specified args. - + 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 perform an in-process test run. - + :param _pytest.config.PytestPluginManager pluginmanager: Pytest plugin manager. :param List[str] args: List of arguments passed on the command line. - """ - - + """ + + def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None: - """(**Deprecated**) modify command line arguments before option parsing. - - This hook is considered deprecated and will be removed in a future pytest version. Consider - using :func:`pytest_load_initial_conftests` instead. - - .. note:: - This hook will not be called for ``conftest.py`` files, only for setuptools plugins. - + """(**Deprecated**) modify command line arguments before option parsing. + + This hook is considered deprecated and will be removed in a future pytest version. Consider + using :func:`pytest_load_initial_conftests` instead. + + .. note:: + This hook will not be called for ``conftest.py`` files, only for setuptools plugins. + :param _pytest.config.Config config: The pytest config object. :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]]: """Called for performing the main command line action. The default - implementation will invoke the configure hooks and runtest_mainloop. - - .. note:: - This hook will not be called for ``conftest.py`` files, only for setuptools plugins. - + implementation will invoke the configure hooks and runtest_mainloop. + + .. note:: + This hook will not be called for ``conftest.py`` files, only for setuptools plugins. + Stops at first non-None result, see :ref:`firstresult`. - + :param _pytest.config.Config config: The pytest config object. - """ - - + """ + + def pytest_load_initial_conftests( early_config: "Config", parser: "Parser", args: List[str] ) -> None: """Called to implement the loading of initial conftest files ahead - of command line option parsing. - - .. note:: - This hook will not be called for ``conftest.py`` files, only for setuptools plugins. - + of command line option parsing. + + .. note:: + This hook will not be called for ``conftest.py`` files, only for setuptools plugins. + :param _pytest.config.Config early_config: The pytest config object. :param List[str] args: Arguments passed on the command line. :param _pytest.config.argparsing.Parser parser: To add command line options. - """ - - -# ------------------------------------------------------------------------- -# collection hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) + """ + + +# ------------------------------------------------------------------------- +# collection hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) def pytest_collection(session: "Session") -> Optional[object]: """Perform the collection phase for the given session. - - 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 default collection phase is this (see individual hooks for full details): 1. Starting from ``session`` as the initial collector: @@ -238,125 +238,125 @@ def pytest_collection(session: "Session") -> Optional[object]: counter (and returns `None`). :param pytest.Session session: The pytest session object. - """ - - + """ + + def pytest_collection_modifyitems( session: "Session", config: "Config", items: List["Item"] ) -> None: """Called after collection has been performed. May filter or re-order - the items in-place. - + the items in-place. + :param pytest.Session session: The pytest session object. :param _pytest.config.Config config: The pytest config object. :param List[pytest.Item] items: List of item objects. - """ - - + """ + + def pytest_collection_finish(session: "Session") -> None: """Called after collection has been performed and modified. - + :param pytest.Session session: The pytest session object. - """ - - -@hookspec(firstresult=True) + """ + + +@hookspec(firstresult=True) def pytest_ignore_collect(path: py.path.local, config: "Config") -> Optional[bool]: """Return True to prevent considering this path for collection. - This hook is consulted for all files and directories prior to calling - more specific hooks. - + This hook is consulted for all files and directories prior to calling + more specific hooks. + Stops at first non-None result, see :ref:`firstresult`. - + :param py.path.local path: The path to analyze. :param _pytest.config.Config config: The pytest config object. - """ - - + """ + + def pytest_collect_file( path: py.path.local, parent: "Collector" ) -> "Optional[Collector]": """Create a Collector for the given path, or None if not relevant. - + The new node needs to have the specified ``parent`` as a parent. - + :param py.path.local path: The path to collect. - """ - - + """ + + # logging hooks for collection - - + + def pytest_collectstart(collector: "Collector") -> None: """Collector starts collecting.""" - - + + def pytest_itemcollected(item: "Item") -> None: """We just collected a test item.""" - - + + def pytest_collectreport(report: "CollectReport") -> None: """Collector finished collecting.""" - - + + def pytest_deselected(items: Sequence["Item"]) -> None: """Called for deselected test items, e.g. by keyword. - + May be called multiple times. """ - - -@hookspec(firstresult=True) + + +@hookspec(firstresult=True) def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]": """Perform ``collector.collect()`` and return a CollectReport. - + Stops at first non-None result, see :ref:`firstresult`. """ - - -# ------------------------------------------------------------------------- -# Python test function related hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) + + +# ------------------------------------------------------------------------- +# Python test function related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) def pytest_pycollect_makemodule(path: py.path.local, parent) -> Optional["Module"]: """Return a Module collector or None for the given path. - This hook will be called for each matching test module path. - The pytest_collect_file hook needs to be used if you want to - create test modules for files that do not match as a test module. - + This hook will be called for each matching test module path. + The pytest_collect_file hook needs to be used if you want to + create test modules for files that do not match as a test module. + Stops at first non-None result, see :ref:`firstresult`. - + :param py.path.local path: The path of module to collect. """ - -@hookspec(firstresult=True) + +@hookspec(firstresult=True) def pytest_pycollect_makeitem( collector: "PyCollector", name: str, obj: object ) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]: """Return a custom item/collector for a Python object in a module, or None. - + Stops at first non-None result, see :ref:`firstresult`. """ - - -@hookspec(firstresult=True) + + +@hookspec(firstresult=True) def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: """Call underlying test function. - + Stops at first non-None result, see :ref:`firstresult`. """ - - + + def pytest_generate_tests(metafunc: "Metafunc") -> None: """Generate (multiple) parametrized calls to a test function.""" - - -@hookspec(firstresult=True) + + +@hookspec(firstresult=True) def pytest_make_parametrize_id( config: "Config", val: object, argname: str ) -> Optional[str]: @@ -364,29 +364,29 @@ def pytest_make_parametrize_id( 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`. - + :param _pytest.config.Config config: The pytest config object. :param val: The parametrized value. :param str argname: The automatic parameter name produced by pytest. - """ - - -# ------------------------------------------------------------------------- + """ + + +# ------------------------------------------------------------------------- # runtest related hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) def pytest_runtestloop(session: "Session") -> Optional[object]: """Perform the main runtest loop (after collection finished). - + The default hook implementation performs the runtest protocol for all items collected in the session (``session.items``), unless the collection failed or the ``collectonly`` pytest option is set. - + If at any point :py:func:`pytest.exit` is called, the loop is terminated immediately. @@ -397,96 +397,96 @@ def pytest_runtestloop(session: "Session") -> Optional[object]: Stops at first non-None result, see :ref:`firstresult`. The return value is not used, but only stops further processing. - """ - - -@hookspec(firstresult=True) + """ + + +@hookspec(firstresult=True) def pytest_runtest_protocol( item: "Item", nextitem: "Optional[Item]" ) -> Optional[object]: """Perform the runtest protocol for a single test item. - + The default runtest protocol is this (see individual hooks for full details): - + - ``pytest_runtest_logstart(nodeid, location)`` - + - Setup phase: - ``call = pytest_runtest_setup(item)`` (wrapped in ``CallInfo(when="setup")``) - ``report = pytest_runtest_makereport(item, call)`` - ``pytest_runtest_logreport(report)`` - ``pytest_exception_interact(call, report)`` if an interactive exception occurred - + - Call phase, if the the setup passed and the ``setuponly`` pytest option is not set: - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``) - ``report = pytest_runtest_makereport(item, call)`` - ``pytest_runtest_logreport(report)`` - ``pytest_exception_interact(call, report)`` if an interactive exception occurred - + - Teardown phase: - ``call = pytest_runtest_teardown(item, nextitem)`` (wrapped in ``CallInfo(when="teardown")``) - ``report = pytest_runtest_makereport(item, call)`` - ``pytest_runtest_logreport(report)`` - ``pytest_exception_interact(call, report)`` if an interactive exception occurred - + - ``pytest_runtest_logfinish(nodeid, location)`` - + :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). - + Stops at first non-None result, see :ref:`firstresult`. The return value is not used, but only stops further processing. - """ - - + """ + + def pytest_runtest_logstart( nodeid: str, location: Tuple[str, Optional[int], str] ) -> None: """Called at the start of running the runtest protocol for a single item. - + See :func:`pytest_runtest_protocol` for a description of the runtest protocol. - + :param str nodeid: Full node ID of the item. :param location: A tuple of ``(filename, lineno, testname)``. - """ - - + """ + + def pytest_runtest_logfinish( nodeid: str, location: Tuple[str, Optional[int], str] ) -> None: """Called at the end of running the runtest protocol for a single item. - + See :func:`pytest_runtest_protocol` for a description of the runtest protocol. - + :param str nodeid: Full node ID of the item. :param location: A tuple of ``(filename, lineno, testname)``. """ - - + + def pytest_runtest_setup(item: "Item") -> None: """Called to perform the setup phase for a test item. - + The default implementation runs ``setup()`` on ``item`` and all of its parents (which haven't been setup yet). This includes obtaining the values of fixtures required by the item (which haven't been obtained yet). - """ - - + """ + + def pytest_runtest_call(item: "Item") -> None: """Called to run the test for test item (the call phase). - + The default implementation calls ``item.runtest()``. """ - - + + def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None: """Called to perform the teardown phase for a test item. - + The default implementation runs the finalizers and calls ``teardown()`` on ``item`` and all of its parents (which need to be torn down). This includes running the teardown phase of fixtures required by the item (if they go out of scope). - + :param nextitem: The scheduled-to-be-next test item (None if no further test item is scheduled). This argument can be used to perform exact teardowns, @@ -533,85 +533,85 @@ def pytest_report_from_serializable( """Restore a report object previously serialized with pytest_report_to_serializable().""" -# ------------------------------------------------------------------------- -# Fixture related hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) +# ------------------------------------------------------------------------- +# Fixture related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) def pytest_fixture_setup( fixturedef: "FixtureDef[Any]", request: "SubRequest" ) -> Optional[object]: """Perform fixture setup execution. - + :returns: The return value of the call to the fixture function. - + Stops at first non-None result, see :ref:`firstresult`. - - .. note:: - If the fixture function returns None, other implementations of - this hook function will continue to be called, according to the - behavior of the :ref:`firstresult` option. - """ - - + + .. note:: + If the fixture function returns None, other implementations of + this hook function will continue to be called, according to the + behavior of the :ref:`firstresult` option. + """ + + def pytest_fixture_post_finalizer( fixturedef: "FixtureDef[Any]", request: "SubRequest" ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not ``None``).""" - - -# ------------------------------------------------------------------------- -# test session related hooks -# ------------------------------------------------------------------------- - - + + +# ------------------------------------------------------------------------- +# test session related hooks +# ------------------------------------------------------------------------- + + def pytest_sessionstart(session: "Session") -> None: """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.Session session: The pytest session object. - """ - - + """ + + def pytest_sessionfinish( session: "Session", exitstatus: Union[int, "ExitCode"], ) -> None: """Called after whole test run finished, right before returning the exit status to the system. - + :param pytest.Session session: The pytest session object. :param int exitstatus: The status which pytest will return to the system. - """ - - + """ + + def pytest_unconfigure(config: "Config") -> None: """Called before test process is exited. - + :param _pytest.config.Config config: The pytest config object. - """ - - -# ------------------------------------------------------------------------- -# hooks for customizing the assert methods -# ------------------------------------------------------------------------- - - + """ + + +# ------------------------------------------------------------------------- +# hooks for customizing the assert methods +# ------------------------------------------------------------------------- + + def pytest_assertrepr_compare( config: "Config", op: str, left: object, right: object ) -> Optional[List[str]]: """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 *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: The pytest config object. - """ - - + """ + + def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None: """**(Experimental)** Called whenever an assertion passes. @@ -647,21 +647,21 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No """ -# ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- # Hooks for influencing reporting (invoked from _pytest_terminal). -# ------------------------------------------------------------------------- - - +# ------------------------------------------------------------------------- + + def pytest_report_header( config: "Config", startdir: py.path.local ) -> Union[str, List[str]]: """Return a string or list of strings to be displayed as header info for terminal reporting. - + :param _pytest.config.Config config: The pytest config object. :param py.path.local startdir: The starting dir. - - .. note:: - + + .. note:: + Lines returned by a plugin are displayed before those of plugins which ran before it. If you want to have your line(s) displayed first, use @@ -669,12 +669,12 @@ def pytest_report_header( .. note:: - This function should be implemented only in plugins or ``conftest.py`` - files situated at the tests root directory due to how pytest - :ref:`discovers plugins during startup <pluginorder>`. - """ - - + This function should be implemented only in plugins or ``conftest.py`` + files situated at the tests root directory due to how pytest + :ref:`discovers plugins during startup <pluginorder>`. + """ + + def pytest_report_collectionfinish( config: "Config", startdir: py.path.local, items: Sequence["Item"], ) -> Union[str, List[str]]: @@ -683,22 +683,22 @@ def pytest_report_collectionfinish( These strings will be displayed after the standard "collected X items" message. - .. versionadded:: 3.2 - + .. versionadded:: 3.2 + :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:: - + Lines returned by a plugin are displayed before those of plugins which ran before it. If you want to have your line(s) displayed first, use :ref:`trylast=True <plugin-hookorder>`. - """ - - -@hookspec(firstresult=True) + """ + + +@hookspec(firstresult=True) def pytest_report_teststatus( report: Union["CollectReport", "TestReport"], config: "Config" ) -> Tuple[ @@ -706,16 +706,16 @@ def pytest_report_teststatus( ]: """Return result-category, shortletter and verbose word for status reporting. - + The result-category is a category in which to count the result, for example "passed", "skipped", "error" or the empty string. The shortletter is shown as testing progresses, for example ".", "s", "E" or the empty string. - + The verbose word is shown as testing progresses in verbose mode, for example "PASSED", "SKIPPED", "ERROR" or the empty string. - + pytest may style these implicitly according to the report outcome. To provide explicit styling, return a tuple for the verbose word, for example ``"rerun", "R", ("RERUN", {"yellow": True})``. @@ -730,17 +730,17 @@ def pytest_report_teststatus( def pytest_terminal_summary( terminalreporter: "TerminalReporter", exitstatus: "ExitCode", config: "Config", ) -> 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 int exitstatus: The exit status that will be reported back to the OS. :param _pytest.config.Config config: The pytest config object. - + .. versionadded:: 4.2 - The ``config`` parameter. - """ - - + The ``config`` parameter. + """ + + @hookspec(historic=True, warn_on_impl=WARNING_CAPTURED_HOOK) def pytest_warning_captured( warning_message: "warnings.WarningMessage", @@ -749,33 +749,33 @@ def pytest_warning_captured( location: Optional[Tuple[str, int, str]], ) -> None: """(**Deprecated**) Process a warning captured by the internal pytest warnings plugin. - + .. deprecated:: 6.0 This hook is considered deprecated and will be removed in a future pytest version. Use :func:`pytest_warning_recorded` instead. - :param warnings.WarningMessage warning_message: - The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains - the same attributes as the parameters of :py:func:`warnings.showwarning`. - - :param str when: - Indicates when the warning was captured. Possible values: - - * ``"config"``: during pytest configuration/initialization stage. - * ``"collect"``: during test collection. - * ``"runtest"``: during test execution. - - :param pytest.Item|None item: - The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. + :param warnings.WarningMessage warning_message: + The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains + the same attributes as the parameters of :py:func:`warnings.showwarning`. + + :param str when: + Indicates when the warning was captured. Possible values: + + * ``"config"``: during pytest configuration/initialization stage. + * ``"collect"``: during test collection. + * ``"runtest"``: during test execution. + + :param pytest.Item|None item: + The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. :param tuple location: When available, holds information about the execution context of the captured warning (filename, linenumber, function). ``function`` evaluates to <module> when the execution context is at the module level. - """ - - + """ + + @hookspec(historic=True) def pytest_warning_recorded( warning_message: "warnings.WarningMessage", @@ -808,47 +808,47 @@ def pytest_warning_recorded( """ -# ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- # Hooks for influencing skipping -# ------------------------------------------------------------------------- - - +# ------------------------------------------------------------------------- + + def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]: """Called when constructing the globals dictionary used for evaluating string conditions in xfail/skipif markers. - + This is useful when the condition for a marker requires objects that are expensive or impossible to obtain during collection time, which is required by normal boolean conditions. - + .. versionadded:: 6.2 - + :param _pytest.config.Config config: The pytest config object. :returns: A dictionary of additional globals to add. """ -# ------------------------------------------------------------------------- -# error handling and internal debugging hooks -# ------------------------------------------------------------------------- - - +# ------------------------------------------------------------------------- +# error handling and internal debugging hooks +# ------------------------------------------------------------------------- + + def pytest_internalerror( excrepr: "ExceptionRepr", excinfo: "ExceptionInfo[BaseException]", ) -> Optional[bool]: """Called for internal errors. - + Return True to suppress the fallback handling of printing an INTERNALERROR message directly to sys.stderr. """ - - + + def pytest_keyboard_interrupt( excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]", ) -> None: """Called for keyboard interrupt.""" - + def pytest_exception_interact( node: Union["Item", "Collector"], @@ -856,8 +856,8 @@ def pytest_exception_interact( report: Union["CollectReport", "TestReport"], ) -> None: """Called when an exception was raised which can potentially be - interactively handled. - + interactively handled. + May be called during collection (see :py:func:`pytest_make_collect_report`), in which case ``report`` is a :py:class:`_pytest.reports.CollectReport`. @@ -866,26 +866,26 @@ def pytest_exception_interact( This hook is not called if the exception that was raised is an internal exception like ``skip.Exception``. - """ - - + """ + + def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None: """Called upon pdb.set_trace(). - + Can be used by plugins to take special action just before the python 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: """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 - debugger leaves interactive mode. - + + Can be used by plugins to take special action just after the python + debugger leaves interactive mode. + :param _pytest.config.Config config: The pytest config object. :param pdb.Pdb pdb: The Pdb instance. - """ + """ diff --git a/contrib/python/pytest/py3/_pytest/junitxml.py b/contrib/python/pytest/py3/_pytest/junitxml.py index dd6a18998c..c4761cd3b8 100644 --- a/contrib/python/pytest/py3/_pytest/junitxml.py +++ b/contrib/python/pytest/py3/_pytest/junitxml.py @@ -1,15 +1,15 @@ """Report test results in JUnit-XML format, 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/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd -""" -import functools -import os +""" +import functools +import os import platform -import re +import re import xml.etree.ElementTree as ET from datetime import datetime from typing import Callable @@ -19,27 +19,27 @@ from typing import Match from typing import Optional from typing import Tuple from typing import Union - -import pytest -from _pytest import nodes + +import pytest +from _pytest import nodes from _pytest import timing from _pytest._code.code import ExceptionRepr from _pytest._code.code import ReprFileLocation from _pytest.config import Config -from _pytest.config import filename_arg +from _pytest.config import filename_arg from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureRequest from _pytest.reports import TestReport from _pytest.store import StoreKey from _pytest.terminal import TerminalReporter - - + + xml_key = StoreKey["LogXML"]() - + def bin_xml_escape(arg: object) -> str: r"""Visually escape invalid XML characters. - + For example, transforms 'hello\aworld\b' into @@ -47,14 +47,14 @@ def bin_xml_escape(arg: object) -> str: Note that the #xABs are *not* XML escapes - missing the ampersand «. The idea is to escape visually for the user rather than for XML itself. """ - + def repl(matchobj: Match[str]) -> str: - i = ord(matchobj.group()) - if i <= 0xFF: + i = ord(matchobj.group()) + if i <= 0xFF: return "#x%02X" % i - else: + else: return "#x%04X" % i - + # The spec range of valid chars is: # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] # For an unknown(?) reason, we disallow #x7F (DEL) as well. @@ -62,8 +62,8 @@ def bin_xml_escape(arg: object) -> str: "[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]" ) return re.sub(illegal_xml_re, repl, str(arg)) - - + + def merge_family(left, right) -> None: result = {} for kl, vl in left.items(): @@ -88,52 +88,52 @@ families["xunit2"] = families["_base"] class _NodeReporter: def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None: - self.id = nodeid - self.xml = xml - self.add_stats = self.xml.add_stats + self.id = nodeid + self.xml = xml + self.add_stats = self.xml.add_stats self.family = self.xml.family - self.duration = 0 + self.duration = 0 self.properties: List[Tuple[str, str]] = [] self.nodes: List[ET.Element] = [] self.attrs: Dict[str, str] = {} - + def append(self, node: ET.Element) -> None: self.xml.add_stats(node.tag) - self.nodes.append(node) - + self.nodes.append(node) + def add_property(self, name: str, value: object) -> None: - self.properties.append((str(name), bin_xml_escape(value))) - + self.properties.append((str(name), bin_xml_escape(value))) + def add_attribute(self, name: str, value: object) -> None: - self.attrs[str(name)] = bin_xml_escape(value) - + self.attrs[str(name)] = bin_xml_escape(value) + def make_properties_node(self) -> Optional[ET.Element]: """Return a Junit node containing custom properties, if any.""" - if self.properties: + if self.properties: properties = ET.Element("properties") for name, value in self.properties: properties.append(ET.Element("property", name=name, value=value)) return properties return None - + def record_testreport(self, testreport: TestReport) -> None: - names = mangle_test_address(testreport.nodeid) - existing_attrs = self.attrs - classnames = names[:-1] - if self.xml.prefix: - classnames.insert(0, self.xml.prefix) + names = mangle_test_address(testreport.nodeid) + existing_attrs = self.attrs + classnames = names[:-1] + if self.xml.prefix: + classnames.insert(0, self.xml.prefix) attrs: Dict[str, str] = { - "classname": ".".join(classnames), - "name": bin_xml_escape(names[-1]), - "file": testreport.location[0], - } - if testreport.location[1] is not None: + "classname": ".".join(classnames), + "name": bin_xml_escape(names[-1]), + "file": testreport.location[0], + } + if testreport.location[1] is not None: attrs["line"] = str(testreport.location[1]) - if hasattr(testreport, "url"): - attrs["url"] = testreport.url - self.attrs = attrs + if hasattr(testreport, "url"): + attrs["url"] = testreport.url + self.attrs = attrs self.attrs.update(existing_attrs) # Restore any user-defined attributes. - + # Preserve legacy testcase behavior. if self.family == "xunit1": return @@ -152,20 +152,20 @@ class _NodeReporter: if properties is not None: testcase.append(properties) testcase.extend(self.nodes) - return testcase - + return testcase + def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None: node = ET.Element(tag, message=message) node.text = bin_xml_escape(data) - self.append(node) - + self.append(node) + def write_captured_output(self, report: TestReport) -> None: if not self.xml.log_passing_tests and report.passed: return - content_out = report.capstdout - content_log = report.caplog - content_err = report.capstderr + content_out = report.capstdout + content_log = report.caplog + content_err = report.capstderr if self.xml.logging == "no": return content_all = "" @@ -181,36 +181,36 @@ class _NodeReporter: content_all = "" if content_all: self._write_content(report, content_all, "system-out") - + def _prepare_content(self, content: str, header: str) -> str: return "\n".join([header.center(80, "-"), content, ""]) - + def _write_content(self, report: TestReport, content: str, jheader: str) -> None: tag = ET.Element(jheader) tag.text = bin_xml_escape(content) self.append(tag) - + def append_pass(self, report: TestReport) -> None: - self.add_stats("passed") - + self.add_stats("passed") + def append_failure(self, report: TestReport) -> None: - # msg = str(report.longrepr.reprtraceback.extraline) - if hasattr(report, "wasxfail"): + # msg = str(report.longrepr.reprtraceback.extraline) + if hasattr(report, "wasxfail"): self._add_simple("skipped", "xfail-marked test passes unexpectedly") - else: + else: assert report.longrepr is not None reprcrash: Optional[ReprFileLocation] = getattr( report.longrepr, "reprcrash", None ) if reprcrash is not None: message = reprcrash.message - else: - message = str(report.longrepr) - message = bin_xml_escape(message) + else: + message = str(report.longrepr) + message = bin_xml_escape(message) self._add_simple("failure", message, str(report.longrepr)) - + def append_collect_error(self, report: TestReport) -> None: - # msg = str(report.longrepr.reprtraceback.extraline) + # msg = str(report.longrepr.reprtraceback.extraline) assert report.longrepr is not None self._add_simple("error", "collection failure", str(report.longrepr)) @@ -221,46 +221,46 @@ class _NodeReporter: assert report.longrepr is not None reprcrash: Optional[ReprFileLocation] = getattr( report.longrepr, "reprcrash", None - ) + ) if reprcrash is not None: reason = reprcrash.message else: reason = str(report.longrepr) - + if report.when == "teardown": msg = f'failed on teardown with "{reason}"' - else: + else: msg = f'failed on setup with "{reason}"' self._add_simple("error", msg, str(report.longrepr)) - + def append_skipped(self, report: TestReport) -> None: - if hasattr(report, "wasxfail"): + if hasattr(report, "wasxfail"): xfailreason = report.wasxfail if xfailreason.startswith("reason: "): xfailreason = xfailreason[8:] xfailreason = bin_xml_escape(xfailreason) skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason) self.append(skipped) - else: + else: assert isinstance(report.longrepr, tuple) - filename, lineno, skipreason = report.longrepr - if skipreason.startswith("Skipped: "): - skipreason = skipreason[9:] + filename, lineno, skipreason = report.longrepr + if skipreason.startswith("Skipped: "): + skipreason = skipreason[9:] details = f"{filename}:{lineno}: {skipreason}" - + skipped = ET.Element("skipped", type="pytest.skip", message=skipreason) skipped.text = bin_xml_escape(details) self.append(skipped) - self.write_captured_output(report) - + self.write_captured_output(report) + def finalize(self) -> None: data = self.to_xml() - self.__dict__.clear() + self.__dict__.clear() # Type ignored becuase mypy doesn't like overriding a method. # Also the return value doesn't match... self.to_xml = lambda: data # type: ignore[assignment] - - + + def _warn_incompatibility_with_xunit2( request: FixtureRequest, fixture_name: str ) -> None: @@ -278,38 +278,38 @@ def _warn_incompatibility_with_xunit2( ) -@pytest.fixture +@pytest.fixture def record_property(request: FixtureRequest) -> Callable[[str, object], None]: """Add extra properties to the calling test. - User properties become part of the test report and are available to the - configured reporters, like JUnit XML. - + User properties become part of the test report and are available to the + configured reporters, like JUnit XML. + The fixture is callable with ``name, value``. The value is automatically XML-encoded. - Example:: - - def test_function(record_property): - record_property("example_key", 1) - """ + Example:: + + def test_function(record_property): + record_property("example_key", 1) + """ _warn_incompatibility_with_xunit2(request, "record_property") - + def append_property(name: str, value: object) -> None: - request.node.user_properties.append((name, value)) - - return append_property - - -@pytest.fixture + request.node.user_properties.append((name, value)) + + return append_property + + +@pytest.fixture def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]: - """Add extra xml attributes to the tag for the calling test. + """Add extra xml attributes to the tag for the calling test. The fixture is callable with ``name, value``. The value is automatically XML-encoded. - """ + """ from _pytest.warning_types import PytestExperimentalApiWarning - + request.node.warn( PytestExperimentalApiWarning("record_xml_attribute is an experimental feature") ) @@ -323,13 +323,13 @@ def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], Non attr_func = add_attr_noop xml = request.config._store.get(xml_key, None) - if xml is not None: - node_reporter = xml.node_reporter(request.node.nodeid) + if xml is not None: + node_reporter = xml.node_reporter(request.node.nodeid) attr_func = node_reporter.add_attribute - + return attr_func - - + + 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 type.""" @@ -337,7 +337,7 @@ def _check_record_param_type(param: str, v: str) -> None: if not isinstance(v, str): msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable] raise TypeError(msg.format(param=param, g=type(v).__name__)) - + @pytest.fixture(scope="session") def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]: @@ -377,33 +377,33 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object] def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("terminal reporting") - group.addoption( - "--junitxml", - "--junit-xml", - action="store", - dest="xmlpath", - metavar="path", - type=functools.partial(filename_arg, optname="--junitxml"), - default=None, - help="create junit-xml style report file at given path.", - ) - group.addoption( - "--junitprefix", - "--junit-prefix", - action="store", - metavar="str", - default=None, - help="prepend prefix to classnames in junit-xml output", - ) - parser.addini( - "junit_suite_name", "Test suite name for JUnit report", default="pytest" - ) - parser.addini( - "junit_logging", - "Write captured log messages to JUnit report: " + group = parser.getgroup("terminal reporting") + group.addoption( + "--junitxml", + "--junit-xml", + action="store", + dest="xmlpath", + metavar="path", + type=functools.partial(filename_arg, optname="--junitxml"), + default=None, + help="create junit-xml style report file at given path.", + ) + group.addoption( + "--junitprefix", + "--junit-prefix", + action="store", + metavar="str", + default=None, + help="prepend prefix to classnames in junit-xml output", + ) + parser.addini( + "junit_suite_name", "Test suite name for JUnit report", default="pytest" + ) + parser.addini( + "junit_logging", + "Write captured log messages to JUnit report: " "one of no|log|system-out|system-err|out-err|all", - default="no", + default="no", ) parser.addini( "junit_log_passing_tests", @@ -421,47 +421,47 @@ def pytest_addoption(parser: Parser) -> None: "Emit XML for schema: one of legacy|xunit1|xunit2", default="xunit2", ) - - + + def pytest_configure(config: Config) -> None: - xmlpath = config.option.xmlpath + xmlpath = config.option.xmlpath # Prevent opening xmllog on worker nodes (xdist). if xmlpath and not hasattr(config, "workerinput"): junit_family = config.getini("junit_family") config._store[xml_key] = LogXML( - xmlpath, - config.option.junitprefix, - config.getini("junit_suite_name"), - config.getini("junit_logging"), + xmlpath, + config.option.junitprefix, + config.getini("junit_suite_name"), + config.getini("junit_logging"), config.getini("junit_duration_report"), junit_family, config.getini("junit_log_passing_tests"), - ) + ) config.pluginmanager.register(config._store[xml_key]) - - + + def pytest_unconfigure(config: Config) -> None: xml = config._store.get(xml_key, None) - if xml: + if xml: del config._store[xml_key] - config.pluginmanager.unregister(xml) - - + config.pluginmanager.unregister(xml) + + def mangle_test_address(address: str) -> List[str]: - path, possible_open_bracket, params = address.partition("[") - names = path.split("::") - try: - names.remove("()") - except ValueError: - pass + path, possible_open_bracket, params = address.partition("[") + names = path.split("::") + try: + names.remove("()") + except ValueError: + pass # 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]) # Put any params back. - names[-1] += possible_open_bracket + params - return names - - + names[-1] += possible_open_bracket + params + return names + + class LogXML: def __init__( self, @@ -473,11 +473,11 @@ class LogXML: family="xunit1", log_passing_tests: bool = True, ) -> None: - logfile = os.path.expanduser(os.path.expandvars(logfile)) - self.logfile = os.path.normpath(os.path.abspath(logfile)) - self.prefix = prefix - self.suite_name = suite_name - self.logging = logging + logfile = os.path.expanduser(os.path.expandvars(logfile)) + self.logfile = os.path.normpath(os.path.abspath(logfile)) + self.prefix = prefix + self.suite_name = suite_name + self.logging = logging self.log_passing_tests = log_passing_tests self.report_duration = report_duration self.family = family @@ -490,177 +490,177 @@ class LogXML: self.node_reporters_ordered: List[_NodeReporter] = [] self.global_properties: List[Tuple[str, str]] = [] - # List of reports that failed on call but teardown is pending. + # List of reports that failed on call but teardown is pending. self.open_reports: List[TestReport] = [] - self.cnt_double_fail_tests = 0 - + self.cnt_double_fail_tests = 0 + # Replaces convenience family with real family. if self.family == "legacy": self.family = "xunit1" def finalize(self, report: TestReport) -> None: - nodeid = getattr(report, "nodeid", report) + nodeid = getattr(report, "nodeid", report) # Local hack to handle xdist report order. workernode = getattr(report, "node", None) reporter = self.node_reporters.pop((nodeid, workernode)) - if reporter is not None: - reporter.finalize() - + if reporter is not None: + reporter.finalize() + def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter: nodeid: Union[str, TestReport] = getattr(report, "nodeid", report) # Local hack to handle xdist report order. workernode = getattr(report, "node", None) - + key = nodeid, workernode - - if key in self.node_reporters: + + if key in self.node_reporters: # TODO: breaks for --dist=each - return self.node_reporters[key] - - reporter = _NodeReporter(nodeid, self) - - self.node_reporters[key] = reporter - self.node_reporters_ordered.append(reporter) - - return reporter - + return self.node_reporters[key] + + reporter = _NodeReporter(nodeid, self) + + self.node_reporters[key] = reporter + self.node_reporters_ordered.append(reporter) + + return reporter + def add_stats(self, key: str) -> None: - if key in self.stats: - self.stats[key] += 1 - + if key in self.stats: + self.stats[key] += 1 + def _opentestcase(self, report: TestReport) -> _NodeReporter: - reporter = self.node_reporter(report) - reporter.record_testreport(report) - return reporter - + reporter = self.node_reporter(report) + reporter.record_testreport(report) + return reporter + def pytest_runtest_logreport(self, report: TestReport) -> None: """Handle a setup/call/teardown report, generating the appropriate XML tags as necessary. - + Note: due to plugins like xdist, this hook may be called in interlaced order with reports from other nodes. For example: - + Usual call order: - -> setup node1 - -> call node1 - -> teardown node1 - -> setup node2 - -> call node2 - -> teardown node2 - + -> setup node1 + -> call node1 + -> teardown node1 + -> setup node2 + -> call node2 + -> teardown node2 + Possible call order in xdist: - -> setup node1 - -> call node1 - -> setup node2 - -> call node2 - -> teardown node2 - -> teardown node1 - """ - close_report = None - if report.passed: - if report.when == "call": # ignore setup/teardown - reporter = self._opentestcase(report) - reporter.append_pass(report) - elif report.failed: - if report.when == "teardown": + -> setup node1 + -> call node1 + -> setup node2 + -> call node2 + -> teardown node2 + -> teardown node1 + """ + close_report = None + if report.passed: + if report.when == "call": # ignore setup/teardown + reporter = self._opentestcase(report) + reporter.append_pass(report) + elif report.failed: + if report.when == "teardown": # The following vars are needed when xdist plugin is used. - report_wid = getattr(report, "worker_id", None) - report_ii = getattr(report, "item_index", None) - close_report = next( - ( - rep - for rep in self.open_reports - if ( - rep.nodeid == report.nodeid - and getattr(rep, "item_index", None) == report_ii - and getattr(rep, "worker_id", None) == report_wid - ) - ), - None, - ) - if close_report: - # We need to open new testcase in case we have failure in - # call and error in teardown in order to follow junit + report_wid = getattr(report, "worker_id", None) + report_ii = getattr(report, "item_index", None) + close_report = next( + ( + rep + for rep in self.open_reports + if ( + rep.nodeid == report.nodeid + and getattr(rep, "item_index", None) == report_ii + and getattr(rep, "worker_id", None) == report_wid + ) + ), + None, + ) + if close_report: + # We need to open new testcase in case we have failure in + # call and error in teardown in order to follow junit # schema. - self.finalize(close_report) - self.cnt_double_fail_tests += 1 - reporter = self._opentestcase(report) - if report.when == "call": - reporter.append_failure(report) - self.open_reports.append(report) + self.finalize(close_report) + self.cnt_double_fail_tests += 1 + reporter = self._opentestcase(report) + if report.when == "call": + reporter.append_failure(report) + self.open_reports.append(report) if not self.log_passing_tests: reporter.write_captured_output(report) - else: - reporter.append_error(report) - elif report.skipped: - reporter = self._opentestcase(report) - reporter.append_skipped(report) - self.update_testcase_duration(report) - if report.when == "teardown": - reporter = self._opentestcase(report) - reporter.write_captured_output(report) - - for propname, propvalue in report.user_properties: + else: + reporter.append_error(report) + elif report.skipped: + reporter = self._opentestcase(report) + reporter.append_skipped(report) + self.update_testcase_duration(report) + if report.when == "teardown": + reporter = self._opentestcase(report) + reporter.write_captured_output(report) + + for propname, propvalue in report.user_properties: reporter.add_property(propname, str(propvalue)) - - self.finalize(report) - report_wid = getattr(report, "worker_id", None) - report_ii = getattr(report, "item_index", None) - close_report = next( - ( - rep - for rep in self.open_reports - if ( - rep.nodeid == report.nodeid - and getattr(rep, "item_index", None) == report_ii - and getattr(rep, "worker_id", None) == report_wid - ) - ), - None, - ) - if close_report: - self.open_reports.remove(close_report) - + + self.finalize(report) + report_wid = getattr(report, "worker_id", None) + report_ii = getattr(report, "item_index", None) + close_report = next( + ( + rep + for rep in self.open_reports + if ( + rep.nodeid == report.nodeid + and getattr(rep, "item_index", None) == report_ii + and getattr(rep, "worker_id", None) == report_wid + ) + ), + None, + ) + if close_report: + self.open_reports.remove(close_report) + def update_testcase_duration(self, report: TestReport) -> None: """Accumulate total duration for nodeid from given report and update the Junit.testcase with the new total if already created.""" if self.report_duration == "total" or report.when == self.report_duration: reporter = self.node_reporter(report) reporter.duration += getattr(report, "duration", 0.0) - + def pytest_collectreport(self, report: TestReport) -> None: - if not report.passed: - reporter = self._opentestcase(report) - if report.failed: - reporter.append_collect_error(report) - else: - reporter.append_collect_skipped(report) - + if not report.passed: + reporter = self._opentestcase(report) + if report.failed: + reporter.append_collect_error(report) + else: + reporter.append_collect_skipped(report) + def pytest_internalerror(self, excrepr: ExceptionRepr) -> None: - reporter = self.node_reporter("internal") - reporter.attrs.update(classname="pytest", name="internal") + reporter = self.node_reporter("internal") + reporter.attrs.update(classname="pytest", name="internal") reporter._add_simple("error", "internal error", str(excrepr)) - + def pytest_sessionstart(self) -> None: self.suite_start_time = timing.time() - + def pytest_sessionfinish(self) -> None: - dirname = os.path.dirname(os.path.abspath(self.logfile)) - if not os.path.isdir(dirname): - os.makedirs(dirname) - logfile = open(self.logfile, "w", encoding="utf-8") + dirname = os.path.dirname(os.path.abspath(self.logfile)) + if not os.path.isdir(dirname): + os.makedirs(dirname) + logfile = open(self.logfile, "w", encoding="utf-8") suite_stop_time = timing.time() - suite_time_delta = suite_stop_time - self.suite_start_time - - numtests = ( - self.stats["passed"] - + self.stats["failure"] - + self.stats["skipped"] - + self.stats["error"] - - self.cnt_double_fail_tests - ) - logfile.write('<?xml version="1.0" encoding="utf-8"?>') - + suite_time_delta = suite_stop_time - self.suite_start_time + + numtests = ( + self.stats["passed"] + + self.stats["failure"] + + self.stats["skipped"] + + self.stats["error"] + - self.cnt_double_fail_tests + ) + logfile.write('<?xml version="1.0" encoding="utf-8"?>') + suite_node = ET.Element( "testsuite", name=self.suite_name, @@ -671,7 +671,7 @@ class LogXML: time="%.3f" % suite_time_delta, timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), hostname=platform.node(), - ) + ) global_properties = self._get_global_properties_node() if global_properties is not None: suite_node.append(global_properties) @@ -680,19 +680,19 @@ class LogXML: testsuites = ET.Element("testsuites") testsuites.append(suite_node) logfile.write(ET.tostring(testsuites, encoding="unicode")) - logfile.close() - + logfile.close() + def pytest_terminal_summary(self, terminalreporter: TerminalReporter) -> None: terminalreporter.write_sep("-", f"generated xml file: {self.logfile}") - + def add_global_property(self, name: str, value: object) -> None: __tracebackhide__ = True _check_record_param_type("name", name) self.global_properties.append((name, bin_xml_escape(value))) - + def _get_global_properties_node(self) -> Optional[ET.Element]: """Return a Junit node containing custom properties, if any.""" - if self.global_properties: + if self.global_properties: properties = ET.Element("properties") for name, value in self.global_properties: properties.append(ET.Element("property", name=name, value=value)) diff --git a/contrib/python/pytest/py3/_pytest/logging.py b/contrib/python/pytest/py3/_pytest/logging.py index 049417ae37..2e4847328a 100644 --- a/contrib/python/pytest/py3/_pytest/logging.py +++ b/contrib/python/pytest/py3/_pytest/logging.py @@ -1,9 +1,9 @@ """Access and control log capturing.""" -import logging +import logging import os -import re +import re import sys -from contextlib import contextmanager +from contextlib import contextmanager from io import StringIO from pathlib import Path from typing import AbstractSet @@ -15,7 +15,7 @@ from typing import Optional from typing import Tuple from typing import TypeVar from typing import Union - + from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager @@ -23,7 +23,7 @@ from _pytest.compat import final from _pytest.compat import nullcontext from _pytest.config import _strtobool from _pytest.config import Config -from _pytest.config import create_terminal_writer +from _pytest.config import create_terminal_writer from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.config.argparsing import Parser @@ -33,65 +33,65 @@ from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.store import StoreKey from _pytest.terminal import TerminalReporter - + DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" -DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" +DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" _ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") caplog_handler_key = StoreKey["LogCaptureHandler"]() caplog_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]() - - + + def _remove_ansi_escape_sequences(text: str) -> str: return _ANSI_ESCAPE_SEQ.sub("", text) -class ColoredLevelFormatter(logging.Formatter): +class ColoredLevelFormatter(logging.Formatter): """A logging formatter which colorizes the %(levelname)..s part of the log format passed to __init__.""" - + LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = { - logging.CRITICAL: {"red"}, - logging.ERROR: {"red", "bold"}, - logging.WARNING: {"yellow"}, - logging.WARN: {"yellow"}, - logging.INFO: {"green"}, - logging.DEBUG: {"purple"}, - logging.NOTSET: set(), + logging.CRITICAL: {"red"}, + logging.ERROR: {"red", "bold"}, + logging.WARNING: {"yellow"}, + logging.WARN: {"yellow"}, + logging.INFO: {"green"}, + logging.DEBUG: {"purple"}, + logging.NOTSET: set(), } LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)") - + def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._original_fmt = self._style._fmt self._level_to_fmt_mapping: Dict[int, str] = {} - + assert self._fmt is not None - levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) - if not levelname_fmt_match: - return - levelname_fmt = levelname_fmt_match.group() - - for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): - formatted_levelname = levelname_fmt % { - "levelname": logging.getLevelName(level) - } - - # add ANSI escape sequences around the formatted levelname - color_kwargs = {name: True for name in color_opts} - colorized_formatted_levelname = terminalwriter.markup( - formatted_levelname, **color_kwargs - ) - self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub( - colorized_formatted_levelname, self._fmt - ) - + levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) + if not levelname_fmt_match: + return + levelname_fmt = levelname_fmt_match.group() + + for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): + formatted_levelname = levelname_fmt % { + "levelname": logging.getLevelName(level) + } + + # add ANSI escape sequences around the formatted levelname + color_kwargs = {name: True for name in color_opts} + colorized_formatted_levelname = terminalwriter.markup( + formatted_levelname, **color_kwargs + ) + self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub( + colorized_formatted_levelname, self._fmt + ) + def format(self, record: logging.LogRecord) -> str: - fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt) + fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt) self._style._fmt = fmt return super().format(record) - - + + class PercentStyleMultiline(logging.PercentStyle): """A logging style with special support for multiline messages. @@ -191,113 +191,113 @@ class PercentStyleMultiline(logging.PercentStyle): def get_option_ini(config: Config, *names: str): - for name in names: - ret = config.getoption(name) # 'default' arg won't work as expected - if ret is None: - ret = config.getini(name) - if ret: - return ret - - + for name in names: + ret = config.getoption(name) # 'default' arg won't work as expected + if ret is None: + ret = config.getini(name) + if ret: + return ret + + def pytest_addoption(parser: Parser) -> None: - """Add options to control log capturing.""" - group = parser.getgroup("logging") - - def add_option_ini(option, dest, default=None, type=None, **kwargs): - parser.addini( - dest, default=default, type=type, help="default value for " + option - ) - group.addoption(option, dest=dest, **kwargs) - - add_option_ini( - "--log-level", - dest="log_level", - default=None, + """Add options to control log capturing.""" + group = parser.getgroup("logging") + + def add_option_ini(option, dest, default=None, type=None, **kwargs): + parser.addini( + dest, default=default, type=type, help="default value for " + option + ) + group.addoption(option, dest=dest, **kwargs) + + add_option_ini( + "--log-level", + dest="log_level", + default=None, metavar="LEVEL", help=( "level of messages to catch/display.\n" "Not set by default, so it depends on the root/parent log handler's" ' effective level, where it is "WARNING" by default.' ), - ) - add_option_ini( - "--log-format", - dest="log_format", - default=DEFAULT_LOG_FORMAT, - help="log format as used by the logging module.", - ) - add_option_ini( - "--log-date-format", - dest="log_date_format", - default=DEFAULT_LOG_DATE_FORMAT, - help="log date format as used by the logging module.", - ) - parser.addini( - "log_cli", - default=False, - type="bool", - help='enable log display during test run (also known as "live logging").', - ) - add_option_ini( - "--log-cli-level", dest="log_cli_level", default=None, help="cli logging level." - ) - add_option_ini( - "--log-cli-format", - dest="log_cli_format", - default=None, - help="log format as used by the logging module.", - ) - add_option_ini( - "--log-cli-date-format", - dest="log_cli_date_format", - default=None, - help="log date format as used by the logging module.", - ) - add_option_ini( - "--log-file", - dest="log_file", - default=None, - help="path to a file when logging will be written to.", - ) - add_option_ini( - "--log-file-level", - dest="log_file_level", - default=None, - help="log file logging level.", - ) - add_option_ini( - "--log-file-format", - dest="log_file_format", - default=DEFAULT_LOG_FORMAT, - help="log format as used by the logging module.", - ) - add_option_ini( - "--log-file-date-format", - dest="log_file_date_format", - default=DEFAULT_LOG_DATE_FORMAT, - help="log date format as used by the logging module.", - ) + ) + add_option_ini( + "--log-format", + dest="log_format", + default=DEFAULT_LOG_FORMAT, + help="log format as used by the logging module.", + ) + add_option_ini( + "--log-date-format", + dest="log_date_format", + default=DEFAULT_LOG_DATE_FORMAT, + help="log date format as used by the logging module.", + ) + parser.addini( + "log_cli", + default=False, + type="bool", + help='enable log display during test run (also known as "live logging").', + ) + add_option_ini( + "--log-cli-level", dest="log_cli_level", default=None, help="cli logging level." + ) + add_option_ini( + "--log-cli-format", + dest="log_cli_format", + default=None, + help="log format as used by the logging module.", + ) + add_option_ini( + "--log-cli-date-format", + dest="log_cli_date_format", + default=None, + help="log date format as used by the logging module.", + ) + add_option_ini( + "--log-file", + dest="log_file", + default=None, + help="path to a file when logging will be written to.", + ) + add_option_ini( + "--log-file-level", + dest="log_file_level", + default=None, + help="log file logging level.", + ) + add_option_ini( + "--log-file-format", + dest="log_file_format", + default=DEFAULT_LOG_FORMAT, + help="log format as used by the logging module.", + ) + add_option_ini( + "--log-file-date-format", + dest="log_file_date_format", + default=DEFAULT_LOG_DATE_FORMAT, + help="log date format as used by the logging module.", + ) add_option_ini( "--log-auto-indent", dest="log_auto_indent", default=None, help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.", ) - - + + _HandlerType = TypeVar("_HandlerType", bound=logging.Handler) # Not using @contextmanager for performance reasons. class catching_logs: - """Context manager that prepares the whole logging machinery properly.""" - + """Context manager that prepares the whole logging machinery properly.""" + __slots__ = ("handler", "level", "orig_level") - + def __init__(self, handler: _HandlerType, level: Optional[int] = None) -> None: self.handler = handler self.level = level - + def __enter__(self): root_logger = logging.getLogger() if self.level is not None: @@ -307,33 +307,33 @@ class catching_logs: self.orig_level = root_logger.level root_logger.setLevel(min(self.orig_level, self.level)) return self.handler - + def __exit__(self, type, value, traceback): root_logger = logging.getLogger() if self.level is not None: root_logger.setLevel(self.orig_level) root_logger.removeHandler(self.handler) - -class LogCaptureHandler(logging.StreamHandler): - """A logging handler that stores log records and the log text.""" - + +class LogCaptureHandler(logging.StreamHandler): + """A logging handler that stores log records and the log text.""" + stream: StringIO def __init__(self) -> None: """Create a new log handler.""" super().__init__(StringIO()) self.records: List[logging.LogRecord] = [] - + def emit(self, record: logging.LogRecord) -> None: - """Keep the log records in a list in addition to the log text.""" - self.records.append(record) + """Keep the log records in a list in addition to the log text.""" + self.records.append(record) super().emit(record) - + def reset(self) -> None: - self.records = [] + self.records = [] self.stream = StringIO() - + def handleError(self, record: logging.LogRecord) -> None: if logging.raiseExceptions: # Fail the test if the log message is bad (emit failed). @@ -341,106 +341,106 @@ class LogCaptureHandler(logging.StreamHandler): # to stderr with the call stack and some extra details. # pytest wants to make such mistakes visible during testing. raise - + @final class LogCaptureFixture: - """Provides access and control of log capturing.""" - + """Provides access and control of log capturing.""" + def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) - self._item = item + self._item = item self._initial_handler_level: Optional[int] = None # Dict of log name -> log level. self._initial_logger_levels: Dict[Optional[str], int] = {} - + def _finalize(self) -> None: """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. if self._initial_handler_level is not None: self.handler.setLevel(self._initial_handler_level) for logger_name, level in self._initial_logger_levels.items(): - logger = logging.getLogger(logger_name) - logger.setLevel(level) - - @property + logger = logging.getLogger(logger_name) + logger.setLevel(level) + + @property def handler(self) -> LogCaptureHandler: """Get the logging handler used by the fixture. - :rtype: LogCaptureHandler - """ + :rtype: LogCaptureHandler + """ return self._item._store[caplog_handler_key] - + def get_records(self, when: str) -> List[logging.LogRecord]: """Get the logging records for one of the possible test phases. - - :param str when: - Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown". - + + :param str when: + 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] - - .. versionadded:: 3.4 - """ + :rtype: List[logging.LogRecord] + + .. versionadded:: 3.4 + """ return self._item._store[caplog_records_key].get(when, []) - - @property + + @property def text(self) -> str: """The formatted log text.""" return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) - - @property + + @property def records(self) -> List[logging.LogRecord]: """The list of log records.""" - return self.handler.records - - @property + return self.handler.records + + @property def record_tuples(self) -> List[Tuple[str, int, str]]: """A list of a stripped down version of log records intended - for use in assertion comparison. - - The format of the tuple is: - - (logger_name, log_level, message) - """ - return [(r.name, r.levelno, r.getMessage()) for r in self.records] - - @property + for use in assertion comparison. + + The format of the tuple is: + + (logger_name, log_level, message) + """ + return [(r.name, r.levelno, r.getMessage()) for r in self.records] + + @property def messages(self) -> List[str]: """A list of format-interpolated log messages. - + Unlike 'records', which contains the format string and parameters for 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 to the logging functions) is not included, as this is added by the formatter in the handler. - .. versionadded:: 3.7 - """ - return [r.getMessage() for r in self.records] - + .. versionadded:: 3.7 + """ + return [r.getMessage() for r in self.records] + def clear(self) -> None: - """Reset the list of log records and the captured log text.""" - self.handler.reset() - + """Reset the list of log records and the captured log text.""" + self.handler.reset() + def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: """Set the level of a logger for the duration of a test. - + .. versionchanged:: 3.4 The levels of the loggers changed by this function will be 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) # Save the original log-level to restore it during teardown. self._initial_logger_levels.setdefault(logger, logger_obj.level) @@ -448,99 +448,99 @@ class LogCaptureFixture: if self._initial_handler_level is None: self._initial_handler_level = self.handler.level self.handler.setLevel(level) - - @contextmanager + + @contextmanager def at_level( self, level: int, logger: Optional[str] = None ) -> Generator[None, None, None]: """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the level is restored to its original value. - + :param int level: The level. :param str logger: The logger to update. If not given, the root logger. - """ + """ logger_obj = logging.getLogger(logger) orig_level = logger_obj.level logger_obj.setLevel(level) handler_orig_level = self.handler.level self.handler.setLevel(level) - try: - yield - finally: + try: + yield + finally: logger_obj.setLevel(orig_level) self.handler.setLevel(handler_orig_level) - - + + @fixture def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]: - """Access and control log capturing. - - Captured logs are available through the following properties/methods:: - + """Access and control log capturing. + + Captured logs are available through the following properties/methods:: + * caplog.messages -> list of format-interpolated log messages - * caplog.text -> string containing formatted log output - * caplog.records -> list of logging.LogRecord instances - * caplog.record_tuples -> list of (logger_name, level, message) tuples - * caplog.clear() -> clear captured records and formatted log output string - """ + * caplog.text -> string containing formatted log output + * caplog.records -> list of logging.LogRecord instances + * caplog.record_tuples -> list of (logger_name, level, message) tuples + * caplog.clear() -> clear captured records and formatted log output string + """ result = LogCaptureFixture(request.node, _ispytest=True) - yield result - result._finalize() - - + yield result + result._finalize() + + def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[int]: - for setting_name in setting_names: - log_level = config.getoption(setting_name) - if log_level is None: - log_level = config.getini(setting_name) - if log_level: - break - else: + for setting_name in setting_names: + log_level = config.getoption(setting_name) + if log_level is None: + log_level = config.getini(setting_name) + if log_level: + break + else: return None - + if isinstance(log_level, str): - log_level = log_level.upper() - try: - return int(getattr(logging, log_level, log_level)) + log_level = log_level.upper() + try: + return int(getattr(logging, log_level, log_level)) except ValueError as e: - # Python logging does not recognise this as a logging level + # Python logging does not recognise this as a logging level raise UsageError( - "'{}' is not recognized as a logging level name for " - "'{}'. Please consider passing the " - "logging level num instead.".format(log_level, setting_name) + "'{}' is not recognized as a logging level name for " + "'{}'. Please consider passing the " + "logging level num instead.".format(log_level, setting_name) ) from e - - + + # run after terminalreporter/capturemanager are configured @hookimpl(trylast=True) def pytest_configure(config: Config) -> None: - config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") - - + config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") + + class LoggingPlugin: """Attaches to the logging module and captures log messages for each test.""" - + def __init__(self, config: Config) -> None: """Create a new plugin to capture log messages. - - The formatter can be safely shared across all handlers so - create a single one for the entire test session here. - """ - self._config = config - + + The formatter can be safely shared across all handlers so + create a single one for the entire test session here. + """ + self._config = config + # Report logging. self.formatter = self._create_formatter( - get_option_ini(config, "log_format"), - get_option_ini(config, "log_date_format"), + get_option_ini(config, "log_format"), + get_option_ini(config, "log_date_format"), get_option_ini(config, "log_auto_indent"), - ) + ) self.log_level = get_log_level_for_setting(config, "log_level") self.caplog_handler = LogCaptureHandler() self.caplog_handler.setFormatter(self.formatter) self.report_handler = LogCaptureHandler() self.report_handler.setFormatter(self.formatter) - + # File logging. self.log_file_level = get_log_level_for_setting(config, "log_file_level") log_file = get_option_ini(config, "log_file") or os.devnull @@ -571,7 +571,7 @@ class LoggingPlugin: self.log_cli_handler: Union[ _LiveLoggingStreamHandler, _LiveLoggingNullHandler ] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) - else: + else: self.log_cli_handler = _LiveLoggingNullHandler() log_cli_formatter = self._create_formatter( get_option_ini(config, "log_cli_format", "log_format"), @@ -579,7 +579,7 @@ class LoggingPlugin: get_option_ini(config, "log_auto_indent"), ) self.log_cli_handler.setFormatter(log_cli_formatter) - + def _create_formatter(self, log_format, log_date_format, auto_indent): # Color option doesn't exist if terminal plugin is disabled. color = getattr(self._config.option, "color", "no") @@ -628,19 +628,19 @@ class LoggingPlugin: if old_stream: old_stream.close() - def _log_cli_enabled(self): + def _log_cli_enabled(self): """Return whether live logging is enabled.""" enabled = self._config.getoption( - "--log-cli-level" - ) is not None or self._config.getini("log_cli") + "--log-cli-level" + ) is not None or self._config.getini("log_cli") if not enabled: return False - + terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") if terminal_reporter is None: # terminal reporter is disabled e.g. by pytest-xdist. return False - + return True @hookimpl(hookwrapper=True, tryfirst=True) @@ -649,8 +649,8 @@ class LoggingPlugin: with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - + yield + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("collection") @@ -664,24 +664,24 @@ class LoggingPlugin: if session.config.option.collectonly: yield return - + if self._log_cli_enabled() and self._config.getoption("verbose") < 1: # The verbose flag is needed to avoid messy test progress output. self._config.option.verbose = 1 - + with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): yield # Run all the tests. - + @hookimpl def pytest_runtest_logstart(self) -> None: self.log_cli_handler.reset() self.log_cli_handler.set_when("start") - + @hookimpl def pytest_runtest_logreport(self) -> None: self.log_cli_handler.set_when("logreport") - + def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: """Implement the internals of the pytest_runtest_xxx() hooks.""" with catching_logs( @@ -693,16 +693,16 @@ class LoggingPlugin: report_handler.reset() item._store[caplog_records_key][when] = caplog_handler.records item._store[caplog_handler_key] = caplog_handler - - yield - + + yield + log = report_handler.stream.getvalue().strip() item.add_report_section(when, "log", log) - + @hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") - + empty: Dict[str, List[logging.LogRecord]] = {} item._store[caplog_records_key] = empty yield from self._runtest_for(item, "setup") @@ -710,7 +710,7 @@ class LoggingPlugin: @hookimpl(hookwrapper=True) def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("call") - + yield from self._runtest_for(item, "call") @hookimpl(hookwrapper=True) @@ -731,8 +731,8 @@ class LoggingPlugin: with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - + yield + @hookimpl def pytest_unconfigure(self) -> None: # Close the FileHandler explicitly. @@ -746,17 +746,17 @@ class _FileHandler(logging.FileHandler): def handleError(self, record: logging.LogRecord) -> None: # Handled by LogCaptureHandler. pass - - -class _LiveLoggingStreamHandler(logging.StreamHandler): + + +class _LiveLoggingStreamHandler(logging.StreamHandler): """A logging StreamHandler used by the live logging feature: it will write a newline before the first log message in each test. - + During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured and won't appear in the terminal. - """ - + """ + # Officially stream needs to be a IO[str], but TerminalReporter # isn't. So force it. stream: TerminalReporter = None # type: ignore @@ -767,39 +767,39 @@ class _LiveLoggingStreamHandler(logging.StreamHandler): capture_manager: Optional[CaptureManager], ) -> None: logging.StreamHandler.__init__(self, stream=terminal_reporter) # type: ignore[arg-type] - self.capture_manager = capture_manager - self.reset() - self.set_when(None) - self._test_outcome_written = False - + self.capture_manager = capture_manager + self.reset() + self.set_when(None) + self._test_outcome_written = False + def reset(self) -> None: """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: """Prepare for the given test phase (setup/call/teardown).""" - self._when = when - self._section_name_shown = False - if when == "start": - self._test_outcome_written = False - + self._when = when + self._section_name_shown = False + if when == "start": + self._test_outcome_written = False + def emit(self, record: logging.LogRecord) -> None: - ctx_manager = ( - self.capture_manager.global_and_fixture_disabled() - if self.capture_manager + ctx_manager = ( + self.capture_manager.global_and_fixture_disabled() + if self.capture_manager else nullcontext() - ) - with ctx_manager: - if not self._first_record_emitted: - self.stream.write("\n") - self._first_record_emitted = True - elif self._when in ("teardown", "finish"): - if not self._test_outcome_written: - self._test_outcome_written = True - self.stream.write("\n") - if not self._section_name_shown and self._when: - self.stream.section("live log " + self._when, sep="-", bold=True) - self._section_name_shown = True + ) + with ctx_manager: + if not self._first_record_emitted: + self.stream.write("\n") + self._first_record_emitted = True + elif self._when in ("teardown", "finish"): + if not self._test_outcome_written: + self._test_outcome_written = True + self.stream.write("\n") + if not self._section_name_shown and self._when: + self.stream.section("live log " + self._when, sep="-", bold=True) + self._section_name_shown = True super().emit(record) def handleError(self, record: logging.LogRecord) -> None: diff --git a/contrib/python/pytest/py3/_pytest/main.py b/contrib/python/pytest/py3/_pytest/main.py index 45bbe346e5..41a33d4494 100644 --- a/contrib/python/pytest/py3/_pytest/main.py +++ b/contrib/python/pytest/py3/_pytest/main.py @@ -1,10 +1,10 @@ """Core implementation of the testing process: init, session, runtest loop.""" import argparse import fnmatch -import functools +import functools import importlib -import os -import sys +import os +import sys from pathlib import Path from typing import Callable from typing import Dict @@ -19,40 +19,40 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union - -import attr -import py - -import _pytest._code -from _pytest import nodes + +import attr +import py + +import _pytest._code +from _pytest import nodes from _pytest.compat import final from _pytest.config import Config -from _pytest.config import directory_arg +from _pytest.config import directory_arg from _pytest.config import ExitCode -from _pytest.config import hookimpl +from _pytest.config import hookimpl from _pytest.config import PytestPluginManager -from _pytest.config import UsageError +from _pytest.config import UsageError from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureManager -from _pytest.outcomes import exit +from _pytest.outcomes import exit from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath from _pytest.pathlib import visit from _pytest.reports import CollectReport from _pytest.reports import TestReport -from _pytest.runner import collect_one_node +from _pytest.runner import collect_one_node from _pytest.runner import SetupState - - + + if TYPE_CHECKING: from typing_extensions import Literal - + def pytest_addoption(parser: Parser) -> None: - parser.addini( - "norecursedirs", - "directory patterns to avoid for recursion", - type="args", + parser.addini( + "norecursedirs", + "directory patterns to avoid for recursion", + type="args", default=[ "*.egg", ".*", @@ -64,22 +64,22 @@ def pytest_addoption(parser: Parser) -> None: "venv", "{arch}", ], - ) - parser.addini( - "testpaths", - "directories to search for tests when no files or directories are given in the " - "command line.", - type="args", - default=[], - ) - group = parser.getgroup("general", "running and selection options") - group._addoption( - "-x", - "--exitfirst", - action="store_const", - dest="maxfail", - const=1, - help="exit instantly on first error or failed test.", + ) + parser.addini( + "testpaths", + "directories to search for tests when no files or directories are given in the " + "command line.", + type="args", + default=[], + ) + group = parser.getgroup("general", "running and selection options") + group._addoption( + "-x", + "--exitfirst", + action="store_const", + dest="maxfail", + const=1, + help="exit instantly on first error or failed test.", ) group = parser.getgroup("pytest-warnings") group.addoption( @@ -95,113 +95,113 @@ def pytest_addoption(parser: Parser) -> None: "warnings.filterwarnings. " "Processed after -W/--pythonwarnings.", ) - group._addoption( - "--maxfail", - metavar="num", - action="store", - type=int, - dest="maxfail", - default=0, - help="exit after first num failures or errors.", - ) - group._addoption( + group._addoption( + "--maxfail", + metavar="num", + action="store", + type=int, + dest="maxfail", + default=0, + help="exit after first num failures or errors.", + ) + group._addoption( "--strict-config", action="store_true", help="any warnings encountered while parsing the `pytest` section of the configuration file raise errors.", ) group._addoption( "--strict-markers", - action="store_true", + action="store_true", help="markers not registered in the `markers` section of the configuration file raise errors.", - ) - group._addoption( + ) + group._addoption( "--strict", action="store_true", help="(deprecated) alias to --strict-markers.", ) group._addoption( - "-c", - metavar="file", - type=str, - dest="inifilename", - help="load configuration from `file` instead of trying to locate one of the implicit " - "configuration files.", - ) - group._addoption( - "--continue-on-collection-errors", - action="store_true", - default=False, - dest="continue_on_collection_errors", - help="Force test execution even if collection errors occur.", - ) - group._addoption( - "--rootdir", - action="store", - dest="rootdir", - help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', " - "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: " - "'$HOME/root_dir'.", - ) - - group = parser.getgroup("collect", "collection") - group.addoption( - "--collectonly", - "--collect-only", + "-c", + metavar="file", + type=str, + dest="inifilename", + help="load configuration from `file` instead of trying to locate one of the implicit " + "configuration files.", + ) + group._addoption( + "--continue-on-collection-errors", + action="store_true", + default=False, + dest="continue_on_collection_errors", + help="Force test execution even if collection errors occur.", + ) + group._addoption( + "--rootdir", + action="store", + dest="rootdir", + help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', " + "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: " + "'$HOME/root_dir'.", + ) + + group = parser.getgroup("collect", "collection") + group.addoption( + "--collectonly", + "--collect-only", "--co", - action="store_true", - help="only collect tests, don't execute them.", + action="store_true", + help="only collect tests, don't execute them.", ) - group.addoption( - "--pyargs", - action="store_true", - help="try to interpret all arguments as python packages.", - ) - group.addoption( - "--ignore", - action="append", - metavar="path", - help="ignore path during collection (multi-allowed).", - ) - group.addoption( + group.addoption( + "--pyargs", + action="store_true", + help="try to interpret all arguments as python packages.", + ) + group.addoption( + "--ignore", + action="append", + metavar="path", + help="ignore path during collection (multi-allowed).", + ) + group.addoption( "--ignore-glob", action="append", metavar="path", help="ignore path pattern during collection (multi-allowed).", ) group.addoption( - "--deselect", - action="append", - metavar="nodeid_prefix", + "--deselect", + action="append", + metavar="nodeid_prefix", help="deselect item (via node id prefix) during collection (multi-allowed).", - ) - group.addoption( - "--confcutdir", - dest="confcutdir", - default=None, - metavar="dir", - type=functools.partial(directory_arg, optname="--confcutdir"), - help="only load conftest.py's relative to specified dir.", - ) - group.addoption( - "--noconftest", - action="store_true", - dest="noconftest", - default=False, - help="Don't load any conftest.py files.", - ) - group.addoption( - "--keepduplicates", - "--keep-duplicates", - action="store_true", - dest="keepduplicates", - default=False, - help="Keep duplicate tests.", - ) - group.addoption( - "--collect-in-virtualenv", - action="store_true", - dest="collect_in_virtualenv", - default=False, - help="Don't ignore tests in a local virtualenv directory", - ) + ) + group.addoption( + "--confcutdir", + dest="confcutdir", + default=None, + metavar="dir", + type=functools.partial(directory_arg, optname="--confcutdir"), + help="only load conftest.py's relative to specified dir.", + ) + group.addoption( + "--noconftest", + action="store_true", + dest="noconftest", + default=False, + help="Don't load any conftest.py files.", + ) + group.addoption( + "--keepduplicates", + "--keep-duplicates", + action="store_true", + dest="keepduplicates", + default=False, + help="Keep duplicate tests.", + ) + group.addoption( + "--collect-in-virtualenv", + action="store_true", + dest="collect_in_virtualenv", + default=False, + help="Don't ignore tests in a local virtualenv directory", + ) group.addoption( "--import-mode", default="prepend", @@ -210,21 +210,21 @@ def pytest_addoption(parser: Parser) -> None: help="prepend/append to sys.path when importing test modules and conftest files, " "default is to prepend.", ) - - group = parser.getgroup("debugconfig", "test session debugging and configuration") - group.addoption( - "--basetemp", - dest="basetemp", - default=None, + + group = parser.getgroup("debugconfig", "test session debugging and configuration") + group.addoption( + "--basetemp", + dest="basetemp", + default=None, type=validate_basetemp, - metavar="dir", - help=( - "base temporary directory for this test run." - "(warning: this directory is removed if it exists)" - ), - ) - - + metavar="dir", + help=( + "base temporary directory for this test run." + "(warning: this directory is removed if it exists)" + ), + ) + + def validate_basetemp(path: str) -> str: # GH 7119 msg = "basetemp must not be empty, the current working directory or any parent directory of it" @@ -259,29 +259,29 @@ def wrap_session( """Skeleton command line program.""" session = Session.from_config(config) session.exitstatus = ExitCode.OK - initstate = 0 - try: - try: - config._do_configure() - initstate = 1 - config.hook.pytest_sessionstart(session=session) - initstate = 2 - session.exitstatus = doit(config, session) or 0 - except UsageError: + initstate = 0 + try: + try: + config._do_configure() + initstate = 1 + config.hook.pytest_sessionstart(session=session) + initstate = 2 + session.exitstatus = doit(config, session) or 0 + except UsageError: session.exitstatus = ExitCode.USAGE_ERROR - raise - except Failed: + raise + except Failed: session.exitstatus = ExitCode.TESTS_FAILED except (KeyboardInterrupt, exit.Exception): excinfo = _pytest._code.ExceptionInfo.from_current() exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED if isinstance(excinfo.value, exit.Exception): - if excinfo.value.returncode is not None: - exitstatus = excinfo.value.returncode + if excinfo.value.returncode is not None: + exitstatus = excinfo.value.returncode if initstate < 2: sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n") - config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - session.exitstatus = exitstatus + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = exitstatus except BaseException: session.exitstatus = ExitCode.INTERNAL_ERROR excinfo = _pytest._code.ExceptionInfo.from_current() @@ -294,12 +294,12 @@ def wrap_session( else: if isinstance(excinfo.value, SystemExit): sys.stderr.write("mainloop: caught unexpected SystemExit!\n") - - finally: + + finally: # Explicitly break reference cycle. excinfo = None # type: ignore - session.startdir.chdir() - if initstate >= 2: + session.startdir.chdir() + if initstate >= 2: try: config.hook.pytest_sessionfinish( session=session, exitstatus=session.exitstatus @@ -308,78 +308,78 @@ def wrap_session( if exc.returncode is not None: session.exitstatus = exc.returncode sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc)) - config._ensure_unconfigure() - return session.exitstatus - - + config._ensure_unconfigure() + return session.exitstatus + + def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]: - return wrap_session(config, _main) - - + return wrap_session(config, _main) + + def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: """Default command line protocol for initialization, session, running tests and reporting.""" - config.hook.pytest_collection(session=session) - config.hook.pytest_runtestloop(session=session) - - if session.testsfailed: + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + + if session.testsfailed: return ExitCode.TESTS_FAILED - elif session.testscollected == 0: + elif session.testscollected == 0: return ExitCode.NO_TESTS_COLLECTED return None - - + + def pytest_collection(session: "Session") -> None: session.perform_collect() - - + + def pytest_runtestloop(session: "Session") -> bool: - if session.testsfailed and not session.config.option.continue_on_collection_errors: + if session.testsfailed and not session.config.option.continue_on_collection_errors: raise session.Interrupted( "%d error%s during collection" % (session.testsfailed, "s" if session.testsfailed != 1 else "") ) - - if session.config.option.collectonly: - return True - - for i, item in enumerate(session.items): - nextitem = session.items[i + 1] if i + 1 < len(session.items) else None - item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) - if session.shouldfail: - raise session.Failed(session.shouldfail) - if session.shouldstop: - raise session.Interrupted(session.shouldstop) - return True - - + + if session.config.option.collectonly: + return True + + for i, item in enumerate(session.items): + nextitem = session.items[i + 1] if i + 1 < len(session.items) else None + item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) + if session.shouldfail: + raise session.Failed(session.shouldfail) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True + + def _in_venv(path: py.path.local) -> bool: """Attempt to detect if ``path`` is the root of a Virtual Environment by checking for the existence of the appropriate activate script.""" - bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") - if not bindir.isdir(): - return False - activates = ( - "activate", - "activate.csh", - "activate.fish", - "Activate", - "Activate.bat", - "Activate.ps1", - ) - return any([fname.basename in activates for fname in bindir.listdir()]) - - + bindir = path.join("Scripts" if sys.platform.startswith("win") else "bin") + if not bindir.isdir(): + return False + activates = ( + "activate", + "activate.csh", + "activate.fish", + "Activate", + "Activate.bat", + "Activate.ps1", + ) + return any([fname.basename in activates for fname in bindir.listdir()]) + + def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool]: - ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath()) - ignore_paths = ignore_paths or [] - excludeopt = config.getoption("ignore") - if excludeopt: - ignore_paths.extend([py.path.local(x) for x in excludeopt]) - - if py.path.local(path) in ignore_paths: - return True - + ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath()) + ignore_paths = ignore_paths or [] + excludeopt = config.getoption("ignore") + if excludeopt: + ignore_paths.extend([py.path.local(x) for x in excludeopt]) + + if py.path.local(path) in ignore_paths: + return True + ignore_globs = config._getconftest_pathlist( "collect_ignore_glob", path=path.dirpath() ) @@ -391,87 +391,87 @@ def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool] if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs): return True - allow_in_venv = config.getoption("collect_in_virtualenv") - if not allow_in_venv and _in_venv(path): - return True + allow_in_venv = config.getoption("collect_in_virtualenv") + if not allow_in_venv and _in_venv(path): + return True return None - - + + def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None: - deselect_prefixes = tuple(config.getoption("deselect") or []) - if not deselect_prefixes: - return - - remaining = [] - deselected = [] - for colitem in items: - if colitem.nodeid.startswith(deselect_prefixes): - deselected.append(colitem) - else: - remaining.append(colitem) - - if deselected: - config.hook.pytest_deselected(items=deselected) - items[:] = remaining - - + deselect_prefixes = tuple(config.getoption("deselect") or []) + if not deselect_prefixes: + return + + remaining = [] + deselected = [] + for colitem in items: + if colitem.nodeid.startswith(deselect_prefixes): + deselected.append(colitem) + else: + remaining.append(colitem) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + class FSHookProxy: def __init__(self, pm: PytestPluginManager, remove_mods) -> None: self.pm = pm self.remove_mods = remove_mods - + def __getattr__(self, name: str): x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) self.__dict__[name] = x return x - -class Interrupted(KeyboardInterrupt): + +class Interrupted(KeyboardInterrupt): """Signals that the test run was interrupted.""" - + __module__ = "builtins" # For py3. - - -class Failed(Exception): + + +class Failed(Exception): """Signals a stop as failed test run.""" - - -@attr.s + + +@attr.s class _bestrelpath_cache(Dict[Path, str]): path = attr.ib(type=Path) - + def __missing__(self, path: Path) -> str: r = bestrelpath(self.path, path) - self[path] = r - return r - - + self[path] = r + return r + + @final -class Session(nodes.FSCollector): - Interrupted = Interrupted - Failed = Failed +class Session(nodes.FSCollector): + Interrupted = Interrupted + Failed = Failed # Set on the session by runner.pytest_sessionstart. _setupstate: SetupState # Set on the session by fixtures.pytest_sessionstart. _fixturemanager: FixtureManager exitstatus: Union[int, ExitCode] - + def __init__(self, config: Config) -> None: super().__init__( config.rootdir, parent=None, config=config, session=self, nodeid="" - ) - self.testsfailed = 0 - self.testscollected = 0 + ) + self.testsfailed = 0 + self.testscollected = 0 self.shouldstop: Union[bool, str] = False self.shouldfail: Union[bool, str] = False - self.trace = config.trace.root.get("collection") + self.trace = config.trace.root.get("collection") self.startdir = config.invocation_dir self._initialpaths: FrozenSet[py.path.local] = frozenset() self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) - self.config.pluginmanager.register(self, name="session") - + self.config.pluginmanager.register(self, name="session") + @classmethod def from_config(cls, config: Config) -> "Session": session: Session = cls._create(config) @@ -488,30 +488,30 @@ class Session(nodes.FSCollector): def _node_location_to_relpath(self, node_path: Path) -> str: # bestrelpath is a quite slow function. - return self._bestrelpathcache[node_path] - - @hookimpl(tryfirst=True) + return self._bestrelpathcache[node_path] + + @hookimpl(tryfirst=True) def pytest_collectstart(self) -> None: - if self.shouldfail: - raise self.Failed(self.shouldfail) - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - @hookimpl(tryfirst=True) + if self.shouldfail: + raise self.Failed(self.shouldfail) + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + @hookimpl(tryfirst=True) def pytest_runtest_logreport( self, report: Union[TestReport, CollectReport] ) -> None: - if report.failed and not hasattr(report, "wasxfail"): - self.testsfailed += 1 - maxfail = self.config.getvalue("maxfail") - if maxfail and self.testsfailed >= maxfail: - self.shouldfail = "stopping after %d failures" % (self.testsfailed) - - pytest_collectreport = pytest_runtest_logreport - + if report.failed and not hasattr(report, "wasxfail"): + self.testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self.testsfailed >= maxfail: + self.shouldfail = "stopping after %d failures" % (self.testsfailed) + + pytest_collectreport = pytest_runtest_logreport + def isinitpath(self, path: py.path.local) -> bool: - return path in self._initialpaths - + return path in self._initialpaths + def gethookproxy(self, fspath: py.path.local): # Check if we have the common case of running # hooks with all conftest.py files. @@ -527,7 +527,7 @@ class Session(nodes.FSCollector): # All plugins are active for this fspath. proxy = self.config.hook return proxy - + def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False @@ -603,10 +603,10 @@ class Session(nodes.FSCollector): self._initial_parts: List[Tuple[py.path.local, List[str]]] = [] self.items: List[nodes.Item] = [] - hook = self.config.hook + hook = self.config.hook items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items - try: + try: initialpaths: List[py.path.local] = [] for arg in args: fspath, parts = resolve_collection_argument( @@ -633,36 +633,36 @@ class Session(nodes.FSCollector): for node in rep.result: self.items.extend(self.genitems(node)) - self.config.pluginmanager.check_pending() - hook.pytest_collection_modifyitems( - session=self, config=self.config, items=items - ) - finally: - hook.pytest_collection_finish(session=self) + self.config.pluginmanager.check_pending() + hook.pytest_collection_modifyitems( + session=self, config=self.config, items=items + ) + finally: + hook.pytest_collection_finish(session=self) + + self.testscollected = len(items) + return items - self.testscollected = len(items) - return items - def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: from _pytest.python import Package - + # Keep track of any collected nodes in here, so we don't duplicate fixtures. node_cache1: Dict[py.path.local, Sequence[nodes.Collector]] = {} node_cache2: Dict[ Tuple[Type[nodes.Collector], py.path.local], nodes.Collector ] = ({}) - + # Keep track of any collected collectors in matchnodes paths, so they # are not collected more than once. matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = ({}) - + # Dirnames of pkgs with dunder-init files. pkg_roots: Dict[str, Package] = {} - + for argpath, names in self._initial_parts: self.trace("processing argument", (argpath, names)) self.trace.root.indent += 1 - + # Start with a Session root, and delve to argpath item (dir or file) # and stack all Packages found on the way. # No point in finding packages when collecting doctests. @@ -675,25 +675,25 @@ class Session(nodes.FSCollector): if parent.isdir(): pkginit = parent.join("__init__.py") if pkginit.isfile() and pkginit not in node_cache1: - col = self._collectfile(pkginit, handle_dupes=False) - if col: - if isinstance(col[0], Package): + col = self._collectfile(pkginit, handle_dupes=False) + if col: + if isinstance(col[0], Package): pkg_roots[str(parent)] = col[0] node_cache1[col[0].fspath] = [col[0]] - + # If it's a directory argument, recurse and look for any Subpackages. # Let the Package collector deal with subnodes, don't collect here. if argpath.check(dir=1): assert not names, "invalid arg {!r}".format((argpath, names)) - + seen_dirs: Set[py.path.local] = set() for direntry in visit(str(argpath), self._recurse): if not direntry.is_file(): continue - + path = py.path.local(direntry.path) dirpath = path.dirpath() - + if dirpath not in seen_dirs: # Collect packages first. seen_dirs.add(dirpath) @@ -714,9 +714,9 @@ class Session(nodes.FSCollector): else: node_cache2[key] = x yield x - else: + else: assert argpath.check(file=1) - + if argpath in node_cache1: col = node_cache1[argpath] else: @@ -724,7 +724,7 @@ class Session(nodes.FSCollector): col = collect_root._collectfile(argpath, handle_dupes=False) if col: node_cache1[argpath] = col - + matching = [] work: List[ Tuple[Sequence[Union[nodes.Item, nodes.Collector]], Sequence[str]] @@ -732,7 +732,7 @@ class Session(nodes.FSCollector): while work: self.trace("matchnodes", col, names) self.trace.root.indent += 1 - + matchnodes, matchnames = work.pop() for node in matchnodes: if not matchnames: @@ -766,15 +766,15 @@ class Session(nodes.FSCollector): # specified in the command line because the module could not be # imported (#134). node.ihook.pytest_collectreport(report=rep) - + self.trace("matchnodes finished -> ", len(matching), "nodes") self.trace.root.indent -= 1 - + if not matching: report_arg = "::".join((str(argpath), *names)) self._notfound.append((report_arg, col)) continue - + # If __init__.py was the only file requested, then the matched # node will be the corresponding Package (by default), and the # first yielded item will be the __init__ Module itself, so @@ -799,17 +799,17 @@ class Session(nodes.FSCollector): def genitems( self, node: Union[nodes.Item, nodes.Collector] ) -> Iterator[nodes.Item]: - self.trace("genitems", node) - if isinstance(node, nodes.Item): - node.ihook.pytest_itemcollected(item=node) - yield node - else: - assert isinstance(node, nodes.Collector) - rep = collect_one_node(node) - if rep.passed: - for subnode in rep.result: + self.trace("genitems", node) + if isinstance(node, nodes.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, nodes.Collector) + rep = collect_one_node(node) + if rep.passed: + for subnode in rep.result: yield from self.genitems(subnode) - node.ihook.pytest_collectreport(report=rep) + node.ihook.pytest_collectreport(report=rep) def search_pypath(module_name: str) -> str: diff --git a/contrib/python/pytest/py3/_pytest/mark/__init__.py b/contrib/python/pytest/py3/_pytest/mark/__init__.py index 6712bedc6e..329a11c4ae 100644 --- a/contrib/python/pytest/py3/_pytest/mark/__init__.py +++ b/contrib/python/pytest/py3/_pytest/mark/__init__.py @@ -6,31 +6,31 @@ from typing import List from typing import Optional from typing import TYPE_CHECKING from typing import Union - + import attr from .expression import Expression from .expression import ParseError -from .structures import EMPTY_PARAMETERSET_OPTION -from .structures import get_empty_parameterset_mark -from .structures import Mark -from .structures import MARK_GEN -from .structures import MarkDecorator -from .structures import MarkGenerator -from .structures import ParameterSet +from .structures import EMPTY_PARAMETERSET_OPTION +from .structures import get_empty_parameterset_mark +from .structures import Mark +from .structures import MARK_GEN +from .structures import MarkDecorator +from .structures import MarkGenerator +from .structures import ParameterSet from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl -from _pytest.config import UsageError +from _pytest.config import UsageError from _pytest.config.argparsing import Parser from _pytest.deprecated import MINUS_K_COLON from _pytest.deprecated import MINUS_K_DASH from _pytest.store import StoreKey - + if TYPE_CHECKING: from _pytest.nodes import Item - - + + __all__ = [ "MARK_GEN", "Mark", @@ -49,86 +49,86 @@ def param( marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), id: Optional[str] = None, ) -> ParameterSet: - """Specify a parameter in `pytest.mark.parametrize`_ calls or - :ref:`parametrized fixtures <fixture-parametrize-marks>`. - - .. code-block:: python - + """Specify a parameter in `pytest.mark.parametrize`_ calls or + :ref:`parametrized fixtures <fixture-parametrize-marks>`. + + .. code-block:: python + @pytest.mark.parametrize( "test_input,expected", [("3+5", 8), pytest.param("6*9", 42, marks=pytest.mark.xfail),], ) - def test_eval(test_input, expected): - assert eval(test_input) == expected - + def test_eval(test_input, expected): + assert eval(test_input) == expected + :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 str id: The id to attribute to this parameter set. - """ + """ return ParameterSet.param(*values, marks=marks, id=id) - - + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group._addoption( - "-k", - action="store", - dest="keyword", - default="", - metavar="EXPRESSION", - help="only run tests which match the given substring expression. " - "An expression is a python evaluatable expression " - "where all names are substring-matched against test names " - "and their parent classes. Example: -k 'test_method or test_" - "other' matches all test functions and classes whose name " - "contains 'test_method' or 'test_other', while -k 'not test_method' " - "matches those that don't contain 'test_method' in their names. " + group = parser.getgroup("general") + group._addoption( + "-k", + action="store", + dest="keyword", + default="", + metavar="EXPRESSION", + help="only run tests which match the given substring expression. " + "An expression is a python evaluatable expression " + "where all names are substring-matched against test names " + "and their parent classes. Example: -k 'test_method or test_" + "other' matches all test functions and classes whose name " + "contains 'test_method' or 'test_other', while -k 'not test_method' " + "matches those that don't contain 'test_method' in their names. " "-k 'not test_method and not test_other' will eliminate the matches. " - "Additionally keywords are matched to classes and functions " - "containing extra names in their 'extra_keyword_matches' set, " + "Additionally keywords are matched to classes and functions " + "containing extra names in their 'extra_keyword_matches' set, " "as well as functions which have names assigned directly to them. " "The matching is case-insensitive.", - ) - - group._addoption( - "-m", - action="store", - dest="markexpr", - default="", - metavar="MARKEXPR", + ) + + group._addoption( + "-m", + action="store", + dest="markexpr", + default="", + metavar="MARKEXPR", help="only run tests matching given mark expression.\n" "For example: -m 'mark1 and not mark2'.", - ) - - group.addoption( - "--markers", - action="store_true", - help="show markers (builtin, plugin and per-project ones).", - ) - - parser.addini("markers", "markers for test functions", "linelist") - parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets") - - + ) + + group.addoption( + "--markers", + action="store_true", + help="show markers (builtin, plugin and per-project ones).", + ) + + parser.addini("markers", "markers for test functions", "linelist") + parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets") + + @hookimpl(tryfirst=True) def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: - import _pytest.config - - if config.option.markers: - config._do_configure() - tw = _pytest.config.create_terminal_writer(config) - for line in config.getini("markers"): - parts = line.split(":", 1) - name = parts[0] - rest = parts[1] if len(parts) == 2 else "" - tw.write("@pytest.mark.%s:" % name, bold=True) - tw.line(rest) - tw.line() - config._ensure_unconfigure() - return 0 - + import _pytest.config + + if config.option.markers: + config._do_configure() + tw = _pytest.config.create_terminal_writer(config) + for line in config.getini("markers"): + parts = line.split(":", 1) + name = parts[0] + rest = parts[1] if len(parts) == 2 else "" + tw.write("@pytest.mark.%s:" % name, bold=True) + tw.line(rest) + tw.line() + config._ensure_unconfigure() + return 0 + return None - + @attr.s(slots=True) class KeywordMatcher: @@ -182,21 +182,21 @@ class KeywordMatcher: def deselect_by_keyword(items: "List[Item]", config: Config) -> None: - keywordexpr = config.option.keyword.lstrip() + keywordexpr = config.option.keyword.lstrip() if not keywordexpr: return - if keywordexpr.startswith("-"): + if keywordexpr.startswith("-"): # To be removed in pytest 7.0.0. warnings.warn(MINUS_K_DASH, stacklevel=2) - keywordexpr = "not " + keywordexpr[1:] - selectuntil = False - if keywordexpr[-1:] == ":": + keywordexpr = "not " + keywordexpr[1:] + selectuntil = False + if keywordexpr[-1:] == ":": # To be removed in pytest 7.0.0. warnings.warn(MINUS_K_COLON, stacklevel=2) - selectuntil = True - keywordexpr = keywordexpr[:-1] - + selectuntil = True + keywordexpr = keywordexpr[:-1] + try: expression = Expression.compile(keywordexpr) except ParseError as e: @@ -204,21 +204,21 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None: f"Wrong expression passed to '-k': {keywordexpr}: {e}" ) from None - remaining = [] - deselected = [] - for colitem in items: + remaining = [] + deselected = [] + for colitem in items: if keywordexpr and not expression.evaluate(KeywordMatcher.from_item(colitem)): - deselected.append(colitem) - else: - if selectuntil: - keywordexpr = None - remaining.append(colitem) - - if deselected: - config.hook.pytest_deselected(items=deselected) - items[:] = remaining - - + deselected.append(colitem) + else: + if selectuntil: + keywordexpr = None + remaining.append(colitem) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + @attr.s(slots=True) class MarkMatcher: """A matcher for markers which are present. @@ -238,45 +238,45 @@ class MarkMatcher: def deselect_by_mark(items: "List[Item]", config: Config) -> None: - matchexpr = config.option.markexpr - if not matchexpr: - return - + matchexpr = config.option.markexpr + if not matchexpr: + return + try: expression = Expression.compile(matchexpr) except ParseError as e: raise UsageError(f"Wrong expression passed to '-m': {matchexpr}: {e}") from None - remaining = [] - deselected = [] - for item in items: + remaining = [] + deselected = [] + for item in items: if expression.evaluate(MarkMatcher.from_item(item)): - remaining.append(item) - else: - deselected.append(item) - - if deselected: - config.hook.pytest_deselected(items=deselected) - items[:] = remaining - - + remaining.append(item) + else: + deselected.append(item) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None: - deselect_by_keyword(items, config) - deselect_by_mark(items, config) - - + deselect_by_keyword(items, config) + deselect_by_mark(items, config) + + def pytest_configure(config: Config) -> None: config._store[old_mark_config_key] = MARK_GEN._config MARK_GEN._config = config - - empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) - - if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""): - raise UsageError( - "{!s} must be one of skip, xfail or fail_at_collect" - " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset) - ) - - + + empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION) + + if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""): + raise UsageError( + "{!s} must be one of skip, xfail or fail_at_collect" + " but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset) + ) + + def pytest_unconfigure(config: Config) -> None: MARK_GEN._config = config._store.get(old_mark_config_key, None) diff --git a/contrib/python/pytest/py3/_pytest/mark/structures.py b/contrib/python/pytest/py3/_pytest/mark/structures.py index 7b1dc46439..f5736a4c1c 100644 --- a/contrib/python/pytest/py3/_pytest/mark/structures.py +++ b/contrib/python/pytest/py3/_pytest/mark/structures.py @@ -1,6 +1,6 @@ import collections.abc -import inspect -import warnings +import inspect +import warnings from typing import Any from typing import Callable from typing import Collection @@ -19,37 +19,37 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - -import attr - + +import attr + from .._code import getfslineno from ..compat import ascii_escaped from ..compat import final -from ..compat import NOTSET +from ..compat import NOTSET from ..compat import NotSetType from _pytest.config import Config -from _pytest.outcomes import fail +from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning - + if TYPE_CHECKING: from ..nodes import Node -EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" - - +EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" + + def istestfunc(func) -> bool: - return ( - hasattr(func, "__call__") - and getattr(func, "__name__", "<lambda>") != "<lambda>" - ) - - + return ( + hasattr(func, "__call__") + and getattr(func, "__name__", "<lambda>") != "<lambda>" + ) + + def get_empty_parameterset_mark( config: Config, argnames: Sequence[str], func ) -> "MarkDecorator": - from ..nodes import Collector - + from ..nodes import Collector + fs, lineno = getfslineno(func) reason = "got empty parameter set %r, function %s at %s:%d" % ( argnames, @@ -58,22 +58,22 @@ def get_empty_parameterset_mark( lineno, ) - requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) - if requested_mark in ("", None, "skip"): + requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) + if requested_mark in ("", None, "skip"): mark = MARK_GEN.skip(reason=reason) - elif requested_mark == "xfail": + elif requested_mark == "xfail": mark = MARK_GEN.xfail(reason=reason, run=False) - elif requested_mark == "fail_at_collect": - f_name = func.__name__ - _, lineno = getfslineno(func) - raise Collector.CollectError( + elif requested_mark == "fail_at_collect": + f_name = func.__name__ + _, lineno = getfslineno(func) + raise Collector.CollectError( "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1) - ) - else: - raise LookupError(requested_mark) + ) + else: + raise LookupError(requested_mark) return mark - - + + class ParameterSet( NamedTuple( "ParameterSet", @@ -84,18 +84,18 @@ class ParameterSet( ], ) ): - @classmethod + @classmethod def param( cls, *values: object, marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (), id: Optional[str] = None, ) -> "ParameterSet": - if isinstance(marks, MarkDecorator): - marks = (marks,) - else: + if isinstance(marks, MarkDecorator): + marks = (marks,) + else: assert isinstance(marks, collections.abc.Collection) - + if id is not None: if not isinstance(id, str): raise TypeError( @@ -103,8 +103,8 @@ class ParameterSet( ) id = ascii_escaped(id) return cls(values, marks, id) - - @classmethod + + @classmethod def extract_from( cls, parameterset: Union["ParameterSet", Sequence[object], object], @@ -112,19 +112,19 @@ class ParameterSet( ) -> "ParameterSet": """Extract from an object or objects. - :param parameterset: + :param parameterset: 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. - + :param force_tuple: Enforce tuple wrapping so single argument tuple values don't get decomposed and break tests. - """ - - if isinstance(parameterset, cls): - return parameterset + """ + + if isinstance(parameterset, cls): + return parameterset if force_tuple: - return cls.param(parameterset) + return cls.param(parameterset) else: # TODO: Refactor to fix this type-ignore. Currently the following # passes type-checking but crashes: @@ -132,7 +132,7 @@ class ParameterSet( # @pytest.mark.parametrize(('x', 'y'), [1, 2]) # def test_foo(x, y): pass return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] - + @staticmethod def _parse_parametrize_args( argnames: Union[str, List[str], Tuple[str, ...]], @@ -140,11 +140,11 @@ class ParameterSet( *args, **kwargs, ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]: - if not isinstance(argnames, (tuple, list)): - argnames = [x.strip() for x in argnames.split(",") if x.strip()] - force_tuple = len(argnames) == 1 - else: - force_tuple = False + if not isinstance(argnames, (tuple, list)): + argnames = [x.strip() for x in argnames.split(",") if x.strip()] + force_tuple = len(argnames) == 1 + else: + force_tuple = False return argnames, force_tuple @staticmethod @@ -154,7 +154,7 @@ class ParameterSet( ) -> List["ParameterSet"]: return [ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues - ] + ] @classmethod def _for_parametrize( @@ -167,18 +167,18 @@ class ParameterSet( ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]: argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) - del argvalues - - if parameters: + del argvalues + + if parameters: # Check all parameter sets have the correct number of values. - for param in parameters: - if len(param.values) != len(argnames): + for param in parameters: + if len(param.values) != len(argnames): msg = ( '{nodeid}: in "parametrize" the number of names ({names_len}):\n' " {names}\n" "must be equal to the number of values ({values_len}):\n" " {values}" - ) + ) fail( msg.format( nodeid=nodeid, @@ -189,26 +189,26 @@ class ParameterSet( ), pytrace=False, ) - else: + else: # Empty parameter set (likely computed at runtime): create a single # parameter set with NOTSET values, with the "empty parameter set" mark applied to it. - mark = get_empty_parameterset_mark(config, argnames, func) - parameters.append( - ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) - ) - return argnames, parameters - - + mark = get_empty_parameterset_mark(config, argnames, func) + parameters.append( + ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) + ) + return argnames, parameters + + @final -@attr.s(frozen=True) +@attr.s(frozen=True) class Mark: #: Name of the mark. - name = attr.ib(type=str) + name = attr.ib(type=str) #: Positional arguments of the mark decorator. args = attr.ib(type=Tuple[Any, ...]) #: Keyword arguments of the mark decorator. kwargs = attr.ib(type=Mapping[str, Any]) - + #: Source Mark for ids with parametrize Marks. _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False) #: Resolved/generated ids with parametrize Marks. @@ -226,9 +226,9 @@ class Mark: Combines by appending args and merging kwargs. :param Mark other: The mark to combine with. - :rtype: Mark - """ - assert self.name == other.name + :rtype: Mark + """ + assert self.name == other.name # Remember source of ids with parametrize Marks. param_ids_from: Optional[Mark] = None @@ -238,37 +238,37 @@ class Mark: elif self._has_param_ids(): param_ids_from = self - return Mark( + return Mark( self.name, self.args + other.args, dict(self.kwargs, **other.kwargs), param_ids_from=param_ids_from, - ) - - + ) + + # A generic parameter designating an object to which a Mark may # be applied -- a test function (callable) or class. # Note: a lambda is not allowed, but this can't be represented. _Markable = TypeVar("_Markable", bound=Union[Callable[..., object], type]) -@attr.s +@attr.s class MarkDecorator: """A decorator for applying a mark on test functions and classes. - + MarkDecorators are created with ``pytest.mark``:: - + mark1 = pytest.mark.NAME # Simple MarkDecorator mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator - and can then be applied as decorators to test functions:: - - @mark2 - def test_function(): - pass - + and can then be applied as decorators to test functions:: + + @mark2 + def test_function(): + pass + When a MarkDecorator is called it does the following: - + 1. If called with a single class as its only positional argument and no additional keyword arguments, it attaches the mark to the class so it gets applied automatically to all test cases found in that class. @@ -281,21 +281,21 @@ class MarkDecorator: 3. When called in any other case, it returns a new MarkDecorator instance with the original MarkDecorator's content updated with the arguments passed to this call. - + Note: The rules above prevent MarkDecorators from storing only a single function or class reference as their positional argument with no additional keyword or positional arguments. You can work around this by using `with_args()`. - """ - + """ + mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark)) - + @property def name(self) -> str: """Alias for mark.name.""" return self.mark.name - - @property + + @property def args(self) -> Tuple[Any, ...]: """Alias for mark.args.""" return self.mark.args @@ -307,22 +307,22 @@ class MarkDecorator: @property def markname(self) -> str: - return self.name # for backward-compat (2.4.1 had this attr) - + return self.name # for backward-compat (2.4.1 had this attr) + def __repr__(self) -> str: return f"<MarkDecorator {self.mark!r}>" - + def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": """Return a MarkDecorator with extra arguments added. - + Unlike calling the MarkDecorator, with_args() can be used even if the sole argument is a callable/class. - + :rtype: MarkDecorator - """ - mark = Mark(self.name, args, kwargs) - return self.__class__(self.mark.combined_with(mark)) - + """ + mark = Mark(self.name, args, kwargs) + return self.__class__(self.mark.combined_with(mark)) + # Type ignored because the overloads overlap with an incompatible # return type. Not much we can do about that. Thankfully mypy picks # the first match so it works out even if we break the rules. @@ -336,29 +336,29 @@ class MarkDecorator: def __call__(self, *args: object, **kwargs: object): """Call the MarkDecorator.""" - if args and not kwargs: - func = args[0] - is_class = inspect.isclass(func) - if len(args) == 1 and (istestfunc(func) or is_class): + if args and not kwargs: + func = args[0] + is_class = inspect.isclass(func) + if len(args) == 1 and (istestfunc(func) or is_class): store_mark(func, self.mark) - return func - return self.with_args(*args, **kwargs) - - + return func + return self.with_args(*args, **kwargs) + + def get_unpacked_marks(obj) -> List[Mark]: """Obtain the unpacked marks that are stored on an object.""" - mark_list = getattr(obj, "pytestmark", []) - if not isinstance(mark_list, list): - mark_list = [mark_list] - return normalize_mark_list(mark_list) - - + mark_list = getattr(obj, "pytestmark", []) + if not isinstance(mark_list, list): + mark_list = [mark_list] + return normalize_mark_list(mark_list) + + def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]: """Normalize marker decorating helpers to mark objects. - + :type List[Union[Mark, Markdecorator]] mark_list: - :rtype: List[Mark] - """ + :rtype: List[Mark] + """ extracted = [ getattr(mark, "mark", mark) for mark in mark_list ] # unpack MarkDecorator @@ -366,19 +366,19 @@ def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List if not isinstance(mark, Mark): raise TypeError(f"got {mark!r} instead of Mark") return [x for x in extracted if isinstance(x, Mark)] - - + + def store_mark(obj, mark: Mark) -> None: """Store a Mark on an object. This is used to implement the Mark declarations/decorators correctly. - """ - assert isinstance(mark, Mark), mark + """ + assert isinstance(mark, Mark), mark # Always reassign name to avoid updating pytestmark in a reference that # was only borrowed. - obj.pytestmark = get_unpacked_marks(obj) + [mark] - - + obj.pytestmark = get_unpacked_marks(obj) + [mark] + + # Typing for builtin pytest marks. This is cheating; it gives builtin marks # special privilege, and breaks modularity. But practicality beats purity... if TYPE_CHECKING: @@ -453,21 +453,21 @@ if TYPE_CHECKING: class MarkGenerator: """Factory for :class:`MarkDecorator` objects - exposed as a ``pytest.mark`` singleton instance. - + Example:: - import pytest + import pytest + + @pytest.mark.slowtest + def test_function(): + pass - @pytest.mark.slowtest - def test_function(): - pass - applies a 'slowtest' :class:`Mark` on ``test_function``. """ - + _config: Optional[Config] = None _markers: Set[str] = set() - + # See TYPE_CHECKING above. if TYPE_CHECKING: skip: _SkipMarkDecorator @@ -478,10 +478,10 @@ class MarkGenerator: filterwarnings: _FilterwarningsMarkDecorator def __getattr__(self, name: str) -> MarkDecorator: - if name[0] == "_": - raise AttributeError("Marker name must NOT start with underscore") + if name[0] == "_": + raise AttributeError("Marker name must NOT start with underscore") - if self._config is not None: + if self._config is not None: # We store a set of markers as a performance optimisation - if a mark # name is in the set we definitely know it, but a mark may be known and # not in the set. We therefore start by updating the set! @@ -518,45 +518,45 @@ class MarkGenerator: 2, ) - return MarkDecorator(Mark(name, (), {})) - - -MARK_GEN = MarkGenerator() - - + return MarkDecorator(Mark(name, (), {})) + + +MARK_GEN = MarkGenerator() + + @final class NodeKeywords(MutableMapping[str, Any]): def __init__(self, node: "Node") -> None: - self.node = node - self.parent = node.parent - self._markers = {node.name: True} - + self.node = node + self.parent = node.parent + self._markers = {node.name: True} + def __getitem__(self, key: str) -> Any: - try: - return self._markers[key] - except KeyError: - if self.parent is None: - raise - return self.parent.keywords[key] - + try: + return self._markers[key] + except KeyError: + if self.parent is None: + raise + return self.parent.keywords[key] + def __setitem__(self, key: str, value: Any) -> None: - self._markers[key] = value - + self._markers[key] = value + def __delitem__(self, key: str) -> None: - raise ValueError("cannot delete key in keywords dict") - + raise ValueError("cannot delete key in keywords dict") + def __iter__(self) -> Iterator[str]: - seen = self._seen() - return iter(seen) - + seen = self._seen() + return iter(seen) + def _seen(self) -> Set[str]: - seen = set(self._markers) - if self.parent is not None: - seen.update(self.parent.keywords) - return seen - + seen = set(self._markers) + if self.parent is not None: + seen.update(self.parent.keywords) + return seen + def __len__(self) -> int: - return len(self._seen()) - + return len(self._seen()) + def __repr__(self) -> str: return f"<NodeKeywords for node {self.node}>" diff --git a/contrib/python/pytest/py3/_pytest/monkeypatch.py b/contrib/python/pytest/py3/_pytest/monkeypatch.py index e5d6c036bc..a052f693ac 100644 --- a/contrib/python/pytest/py3/_pytest/monkeypatch.py +++ b/contrib/python/pytest/py3/_pytest/monkeypatch.py @@ -1,9 +1,9 @@ """Monkeypatching and mocking functionality.""" -import os -import re -import sys -import warnings -from contextlib import contextmanager +import os +import re +import sys +import warnings +from contextlib import contextmanager from pathlib import Path from typing import Any from typing import Generator @@ -14,101 +14,101 @@ from typing import overload from typing import Tuple from typing import TypeVar from typing import Union - + from _pytest.compat import final -from _pytest.fixtures import fixture +from _pytest.fixtures import fixture from _pytest.warning_types import PytestWarning - -RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") - - + +RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") + + K = TypeVar("K") V = TypeVar("V") -@fixture +@fixture def monkeypatch() -> Generator["MonkeyPatch", None, None]: """A convenient fixture for monkey-patching. - + The fixture provides these methods to modify objects, dictionaries or os.environ:: - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=False) - monkeypatch.delenv(name, raising=True) - monkeypatch.syspath_prepend(path) - monkeypatch.chdir(path) - + monkeypatch.setattr(obj, name, value, raising=True) + monkeypatch.delattr(obj, name, raising=True) + monkeypatch.setitem(mapping, name, value) + monkeypatch.delitem(obj, name, raising=True) + monkeypatch.setenv(name, value, prepend=False) + monkeypatch.delenv(name, raising=True) + monkeypatch.syspath_prepend(path) + monkeypatch.chdir(path) + All modifications will be undone after the requesting test function or fixture has finished. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. - """ - mpatch = MonkeyPatch() - yield mpatch - mpatch.undo() - - + """ + mpatch = MonkeyPatch() + yield mpatch + mpatch.undo() + + def resolve(name: str) -> object: # Simplified from zope.dottedname. - parts = name.split(".") - - used = parts.pop(0) - found = __import__(used) - for part in parts: - used += "." + part - try: - found = getattr(found, part) - except AttributeError: - pass - else: - continue + parts = name.split(".") + + used = parts.pop(0) + found = __import__(used) + for part in parts: + used += "." + part + try: + found = getattr(found, part) + except AttributeError: + pass + else: + continue # We use explicit un-nesting of the handling block in order # to avoid nested exceptions. - try: - __import__(used) - except ImportError as ex: - expected = str(ex).split()[-1] - if expected == used: - raise - else: + try: + __import__(used) + except ImportError as ex: + expected = str(ex).split()[-1] + if expected == used: + raise + else: raise ImportError(f"import error in {used}: {ex}") from ex - found = annotated_getattr(found, part, used) - return found - - + found = annotated_getattr(found, part, used) + return found + + def annotated_getattr(obj: object, name: str, ann: str) -> object: - try: - obj = getattr(obj, name) + try: + obj = getattr(obj, name) except AttributeError as e: - raise AttributeError( + raise AttributeError( "{!r} object at {} has no attribute {!r}".format( type(obj).__name__, ann, name ) ) from e - return obj - - + return obj + + def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]: if not isinstance(import_path, str) or "." not in import_path: # type: ignore[unreachable] raise TypeError(f"must be absolute import path string, not {import_path!r}") - module, attr = import_path.rsplit(".", 1) - target = resolve(module) - if raising: - annotated_getattr(target, attr, ann=module) - return attr, target - - + module, attr = import_path.rsplit(".", 1) + target = resolve(module) + if raising: + annotated_getattr(target, attr, ann=module) + return attr, target + + class Notset: def __repr__(self) -> str: - return "<notset>" - - -notset = Notset() - - + return "<notset>" + + +notset = Notset() + + @final class MonkeyPatch: """Helper to conveniently monkeypatch attributes/items/environment @@ -121,47 +121,47 @@ class MonkeyPatch: the fixture is not available. In this case, use :meth:`with MonkeyPatch.context() as mp: <context>` or remember to call :meth:`undo` explicitly. - """ - + """ + def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = ([]) self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None - + @classmethod - @contextmanager + @contextmanager def context(cls) -> Generator["MonkeyPatch", None, None]: """Context manager that returns a new :class:`MonkeyPatch` object which undoes any patching done inside the ``with`` block upon exit. - + Example: - .. code-block:: python - - import functools + .. code-block:: python + + import functools + + def test_partial(monkeypatch): + with monkeypatch.context() as m: + m.setattr(functools, "partial", 3) - def test_partial(monkeypatch): - with monkeypatch.context() as m: - m.setattr(functools, "partial", 3) - - Useful in situations where it is desired to undo some patches before the test ends, - such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples - of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_. - """ + Useful in situations where it is desired to undo some patches before the test ends, + such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples + of this see `#3290 <https://github.com/pytest-dev/pytest/issues/3290>`_. + """ m = cls() - try: - yield m - finally: - m.undo() - + try: + yield m + finally: + m.undo() + @overload def setattr( self, target: str, name: object, value: Notset = ..., raising: bool = ..., ) -> None: ... - + @overload def setattr( self, target: object, name: str, value: object, raising: bool = ..., @@ -177,27 +177,27 @@ class MonkeyPatch: ) -> None: """Set attribute value on target, memorizing the old value. - For convenience you can specify a string as ``target`` which - will be interpreted as a dotted import path, with the last part + For convenience you can specify a string as ``target`` which + will be interpreted as a dotted import path, with the last part being the attribute name. For example, - ``monkeypatch.setattr("os.getcwd", lambda: "/")`` - would set the ``getcwd`` function of the ``os`` module. - + ``monkeypatch.setattr("os.getcwd", lambda: "/")`` + would set the ``getcwd`` function of the ``os`` module. + Raises AttributeError if the attribute does not exist, unless ``raising`` is set to False. - """ - __tracebackhide__ = True - import inspect - + """ + __tracebackhide__ = True + import inspect + if isinstance(value, Notset): if not isinstance(target, str): - raise TypeError( - "use setattr(target, name, value) or " - "setattr(target, value) with target being a dotted " - "import string" - ) - value = name - name, target = derive_importpath(target, raising) + raise TypeError( + "use setattr(target, name, value) or " + "setattr(target, value) with target being a dotted " + "import string" + ) + value = name + name, target = derive_importpath(target, raising) else: if not isinstance(name, str): raise TypeError( @@ -205,17 +205,17 @@ class MonkeyPatch: "setattr(target, value) with target being a dotted " "import string" ) - - oldval = getattr(target, name, notset) - if raising and oldval is notset: + + oldval = getattr(target, name, notset) + if raising and oldval is notset: raise AttributeError(f"{target!r} has no attribute {name!r}") - - # avoid class descriptors like staticmethod/classmethod - if inspect.isclass(target): - oldval = target.__dict__.get(name, notset) - self._setattr.append((target, name, oldval)) - setattr(target, name, value) - + + # avoid class descriptors like staticmethod/classmethod + if inspect.isclass(target): + oldval = target.__dict__.get(name, notset) + self._setattr.append((target, name, oldval)) + setattr(target, name, value) + def delattr( self, target: Union[object, str], @@ -223,55 +223,55 @@ class MonkeyPatch: raising: bool = True, ) -> None: """Delete attribute ``name`` from ``target``. - - If no ``name`` is specified and ``target`` is a string - it will be interpreted as a dotted import path with the - last part being the attribute name. - + + If no ``name`` is specified and ``target`` is a string + it will be interpreted as a dotted import path with the + last part being the attribute name. + Raises AttributeError it the attribute does not exist, unless ``raising`` is set to False. - """ - __tracebackhide__ = True + """ + __tracebackhide__ = True import inspect if isinstance(name, Notset): if not isinstance(target, str): - raise TypeError( - "use delattr(target, name) or " - "delattr(target) with target being a dotted " - "import string" - ) - name, target = derive_importpath(target, raising) - - if not hasattr(target, name): - if raising: - raise AttributeError(name) - else: + raise TypeError( + "use delattr(target, name) or " + "delattr(target) with target being a dotted " + "import string" + ) + name, target = derive_importpath(target, raising) + + if not hasattr(target, name): + if raising: + raise AttributeError(name) + else: oldval = getattr(target, name, notset) # Avoid class descriptors like staticmethod/classmethod. if inspect.isclass(target): oldval = target.__dict__.get(name, notset) self._setattr.append((target, name, oldval)) - delattr(target, name) - + delattr(target, name) + def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: """Set dictionary entry ``name`` to value.""" - self._setitem.append((dic, name, dic.get(name, notset))) - dic[name] = value - + self._setitem.append((dic, name, dic.get(name, notset))) + dic[name] = value + def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: """Delete ``name`` from dict. - + Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to False. - """ - if name not in dic: - if raising: - raise KeyError(name) - else: - self._setitem.append((dic, name, dic.get(name, notset))) - del dic[name] - + """ + if name not in dic: + if raising: + raise KeyError(name) + else: + self._setitem.append((dic, name, dic.get(name, notset))) + del dic[name] + def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: """Set environment variable ``name`` to ``value``. @@ -279,38 +279,38 @@ class MonkeyPatch: value and prepend the ``value`` adjoined with the ``prepend`` character. """ - if not isinstance(value, str): + if not isinstance(value, str): warnings.warn( # type: ignore[unreachable] PytestWarning( - "Value of environment variable {name} type should be str, but got " - "{value!r} (type: {type}); converted to str implicitly".format( - name=name, value=value, type=type(value).__name__ - ) - ), - stacklevel=2, - ) - value = str(value) - if prepend and name in os.environ: - value = value + prepend + os.environ[name] - self.setitem(os.environ, name, value) - + "Value of environment variable {name} type should be str, but got " + "{value!r} (type: {type}); converted to str implicitly".format( + name=name, value=value, type=type(value).__name__ + ) + ), + stacklevel=2, + ) + value = str(value) + if prepend and name in os.environ: + value = value + prepend + os.environ[name] + self.setitem(os.environ, name, value) + def delenv(self, name: str, raising: bool = True) -> None: """Delete ``name`` from the environment. - + Raises ``KeyError`` if it does not exist, unless ``raising`` is set to False. - """ + """ environ: MutableMapping[str, str] = os.environ self.delitem(environ, name, raising=raising) - + def syspath_prepend(self, path) -> None: """Prepend ``path`` to ``sys.path`` list of import locations.""" from pkg_resources import fixup_namespace_packages - if self._savesyspath is None: - self._savesyspath = sys.path[:] - sys.path.insert(0, str(path)) - + if self._savesyspath is None: + self._savesyspath = sys.path[:] + sys.path.insert(0, str(path)) + # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 fixup_namespace_packages(str(path)) @@ -328,52 +328,52 @@ class MonkeyPatch: def chdir(self, path) -> None: """Change the current working directory to the specified path. - Path can be a string or a py.path.local object. - """ - if self._cwd is None: - self._cwd = os.getcwd() - if hasattr(path, "chdir"): - path.chdir() - elif isinstance(path, Path): + Path can be a string or a py.path.local object. + """ + if self._cwd is None: + self._cwd = os.getcwd() + if hasattr(path, "chdir"): + path.chdir() + elif isinstance(path, Path): # Modern python uses the fspath protocol here LEGACY - os.chdir(str(path)) - else: - os.chdir(path) - + os.chdir(str(path)) + else: + os.chdir(path) + def undo(self) -> None: """Undo previous changes. - + 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 - called automatically during tear-down. - - Note that the same `monkeypatch` fixture is used across a - single test function invocation. If `monkeypatch` is used both by - the test function itself and one of the test fixtures, - calling `undo()` will undo all of the changes made in - both functions. - """ - for obj, name, value in reversed(self._setattr): - if value is not notset: - setattr(obj, name, value) - else: - delattr(obj, name) - self._setattr[:] = [] + There is generally no need to call `undo()`, since it is + called automatically during tear-down. + + Note that the same `monkeypatch` fixture is used across a + single test function invocation. If `monkeypatch` is used both by + the test function itself and one of the test fixtures, + calling `undo()` will undo all of the changes made in + both functions. + """ + for obj, name, value in reversed(self._setattr): + if value is not notset: + setattr(obj, name, value) + else: + delattr(obj, name) + self._setattr[:] = [] for dictionary, key, value in reversed(self._setitem): - if value is notset: - try: + if value is notset: + try: del dictionary[key] - except KeyError: + except KeyError: pass # Was already deleted, so we have the desired state. - else: + else: dictionary[key] = value - self._setitem[:] = [] - if self._savesyspath is not None: - sys.path[:] = self._savesyspath - self._savesyspath = None - - if self._cwd is not None: - os.chdir(self._cwd) - self._cwd = None + self._setitem[:] = [] + if self._savesyspath is not None: + sys.path[:] = self._savesyspath + self._savesyspath = None + + if self._cwd is not None: + os.chdir(self._cwd) + self._cwd = None diff --git a/contrib/python/pytest/py3/_pytest/nodes.py b/contrib/python/pytest/py3/_pytest/nodes.py index a4a4b5b57c..27434fb6a6 100644 --- a/contrib/python/pytest/py3/_pytest/nodes.py +++ b/contrib/python/pytest/py3/_pytest/nodes.py @@ -1,5 +1,5 @@ -import os -import warnings +import os +import warnings from pathlib import Path from typing import Callable from typing import Iterable @@ -13,10 +13,10 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - -import py - -import _pytest._code + +import py + +import _pytest._code from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr @@ -26,40 +26,40 @@ from _pytest.config import ConftestImportFailure from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator -from _pytest.mark.structures import NodeKeywords -from _pytest.outcomes import fail +from _pytest.mark.structures import NodeKeywords +from _pytest.outcomes import fail from _pytest.pathlib import absolutepath from _pytest.store import Store - + if TYPE_CHECKING: # Imported here due to circular import. from _pytest.main import Session from _pytest._code.code import _TracebackStyle -SEP = "/" - -tracebackcutdir = py.path.local(_pytest.__file__).dirpath() - - +SEP = "/" + +tracebackcutdir = py.path.local(_pytest.__file__).dirpath() + + def iterparentnodeids(nodeid: str) -> Iterator[str]: """Return the parent node IDs of a given node ID, inclusive. - + For the node ID - + "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" - + the result would be - + "" "testing" "testing/code" "testing/code/test_excinfo.py" "testing/code/test_excinfo.py::TestFormattedExcinfo" "testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source" - + Note that :: parts are only considered at the last / component. - """ + """ pos = 0 sep = SEP yield "" @@ -75,8 +75,8 @@ def iterparentnodeids(nodeid: str) -> Iterator[str]: if at: yield nodeid[:at] pos = at + len(sep) - - + + _NodeType = TypeVar("_NodeType", bound="Node") @@ -97,7 +97,7 @@ class NodeMeta(type): class Node(metaclass=NodeMeta): """Base class for Collector and Item, the components of the test collection tree. - + Collector subclasses have children; Items are leaf nodes. """ @@ -114,7 +114,7 @@ class Node(metaclass=NodeMeta): "__dict__", ) - def __init__( + def __init__( self, name: str, parent: "Optional[Node]" = None, @@ -124,11 +124,11 @@ class Node(metaclass=NodeMeta): nodeid: Optional[str] = None, ) -> None: #: A unique name within the scope of the parent node. - self.name = name - + self.name = name + #: The parent collector node. - self.parent = parent - + self.parent = parent + #: The pytest config object. if config: self.config: Config = config @@ -136,7 +136,7 @@ class Node(metaclass=NodeMeta): if not parent: raise TypeError("config or parent must be provided") self.config = parent.config - + #: The pytest session this node is part of. if session: self.session = session @@ -144,29 +144,29 @@ class Node(metaclass=NodeMeta): if not parent: raise TypeError("session or parent must be provided") self.session = parent.session - + #: 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. - self.keywords = NodeKeywords(self) - + self.keywords = NodeKeywords(self) + #: The marker objects belonging to this node. self.own_markers: List[Mark] = [] - + #: Allow adding of extra keywords to use for matching. self.extra_keyword_matches: Set[str] = set() - - if nodeid is not None: - assert "::()" not in nodeid - self._nodeid = nodeid - else: + + if nodeid is not None: + assert "::()" not in nodeid + self._nodeid = nodeid + else: if not self.parent: raise TypeError("nodeid or parent must be provided") - self._nodeid = self.parent.nodeid - if self.name != "()": - self._nodeid += "::" + self.name - + self._nodeid = self.parent.nodeid + if self.name != "()": + self._nodeid += "::" + self.name + # A place where plugins can store information on the node for their # own use. Currently only intended for internal plugins. self._store = Store() @@ -189,121 +189,121 @@ class Node(metaclass=NodeMeta): raise TypeError("session is not a valid argument for from_parent") return cls._create(parent=parent, **kw) - @property - def ihook(self): + @property + def ihook(self): """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: return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) - + def warn(self, warning: Warning) -> None: """Issue a warning for this Node. - + Warnings will be displayed after the test session, unless explicitly suppressed. - + :param Warning warning: The warning instance to issue. - + :raises ValueError: If ``warning`` instance is not a subclass of Warning. - + Example usage: - - .. code-block:: python - - node.warn(PytestWarning("some message")) + + .. code-block:: python + + node.warn(PytestWarning("some message")) node.warn(UserWarning("some message")) - + .. versionchanged:: 6.2 Any subclass of :class:`Warning` is now accepted, rather than only :class:`PytestWarning <pytest.PytestWarning>` subclasses. - """ + """ # enforce type checks here to avoid getting a generic type error later otherwise. if not isinstance(warning, Warning): - raise ValueError( + raise ValueError( "warning must be an instance of Warning or subclass, got {!r}".format( - warning - ) - ) - path, lineno = get_fslocation_from_item(self) + warning + ) + ) + path, lineno = get_fslocation_from_item(self) assert lineno is not None - warnings.warn_explicit( + warnings.warn_explicit( warning, category=None, filename=str(path), lineno=lineno + 1, - ) - + ) + # Methods for ordering nodes. - @property + @property def nodeid(self) -> str: """A ::-separated string denoting its collection tree address.""" - return self._nodeid - + return self._nodeid + def __hash__(self) -> int: return hash(self._nodeid) - + def setup(self) -> None: - pass - + pass + def teardown(self) -> None: - pass - + pass + def listchain(self) -> List["Node"]: """Return list of all parent collectors up to self, starting from the root of collection tree.""" - chain = [] + chain = [] item: Optional[Node] = self - while item is not None: - chain.append(item) - item = item.parent - chain.reverse() - return chain - + while item is not None: + chain.append(item) + item = item.parent + chain.reverse() + return chain + def add_marker( self, marker: Union[str, MarkDecorator], append: bool = True ) -> None: """Dynamically add a marker object to the node. - + :param append: Whether to append the marker, or prepend it. - """ + """ from _pytest.mark import MARK_GEN - + if isinstance(marker, MarkDecorator): marker_ = marker elif isinstance(marker, str): marker_ = getattr(MARK_GEN, marker) else: - raise ValueError("is not a string or pytest.mark.* Marker") + raise ValueError("is not a string or pytest.mark.* Marker") self.keywords[marker_.name] = marker_ - if append: + if append: self.own_markers.append(marker_.mark) - else: + else: self.own_markers.insert(0, marker_.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. - """ - 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( self, name: Optional[str] = None ) -> Iterator[Tuple["Node", Mark]]: """Iterate over all markers of the node. - + :param name: If given, filter the results by the name attribute. :returns: An iterator of (node, mark) tuples. - """ - for node in reversed(self.listchain()): - for mark in node.own_markers: - if name is None or getattr(mark, "name", None) == name: - yield node, mark - + """ + for node in reversed(self.listchain()): + for mark in node.own_markers: + if name is None or getattr(mark, "name", None) == name: + yield node, mark + @overload def get_closest_marker(self, name: str) -> Optional[Mark]: ... - + @overload def get_closest_marker(self, name: str, default: Mark) -> Mark: ... @@ -316,39 +316,39 @@ class Node(metaclass=NodeMeta): :param default: Fallback return value if no marker was found. :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]: """Return a set of all extra keywords in self and any parents.""" extra_keywords: Set[str] = set() - for item in self.listchain(): - extra_keywords.update(item.extra_keyword_matches) - return extra_keywords - + for item in self.listchain(): + extra_keywords.update(item.extra_keyword_matches) + return extra_keywords + def listnames(self) -> List[str]: - return [x.name for x in self.listchain()] - + return [x.name for x in self.listchain()] + def addfinalizer(self, fin: Callable[[], object]) -> None: """Register a function to be called when this node is finalized. - - This method can only be called when this node is active - in a setup chain, for example during self.setup(). - """ - self.session._setupstate.addfinalizer(fin, self) - + + This method can only be called when this node is active + in a setup chain, for example during self.setup(). + """ + self.session._setupstate.addfinalizer(fin, self) + def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]: """Get the next parent node (including self) which is an instance of the given class.""" current: Optional[Node] = self - while current and not isinstance(current, cls): - current = current.parent + while current and not isinstance(current, cls): + current = current.parent assert current is None or isinstance(current, cls) - return current - + return current + def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: - pass - + pass + def _repr_failure_py( self, excinfo: ExceptionInfo[BaseException], @@ -359,51 +359,51 @@ class Node(metaclass=NodeMeta): if isinstance(excinfo.value, ConftestImportFailure): excinfo = ExceptionInfo(excinfo.value.excinfo) if isinstance(excinfo.value, fail.Exception): - if not excinfo.value.pytrace: + if not excinfo.value.pytrace: style = "value" if isinstance(excinfo.value, FixtureLookupError): - return excinfo.value.formatrepr() + return excinfo.value.formatrepr() if self.config.getoption("fulltrace", False): - style = "long" - else: - tb = _pytest._code.Traceback([excinfo.traceback[-1]]) - self._prunetraceback(excinfo) - if len(excinfo.traceback) == 0: - excinfo.traceback = tb - if style == "auto": - style = "long" - # XXX should excinfo.getrepr record all data and toterminal() process it? - if style is None: + style = "long" + else: + tb = _pytest._code.Traceback([excinfo.traceback[-1]]) + self._prunetraceback(excinfo) + if len(excinfo.traceback) == 0: + excinfo.traceback = tb + if style == "auto": + style = "long" + # XXX should excinfo.getrepr record all data and toterminal() process it? + if style is None: if self.config.getoption("tbstyle", "auto") == "short": - style = "short" - else: - style = "long" - + style = "short" + else: + style = "long" + if self.config.getoption("verbose", 0) > 1: - truncate_locals = False - else: - truncate_locals = True - + truncate_locals = False + else: + truncate_locals = True + # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. # It is possible for a fixture/test to change the CWD while this code runs, which # would then result in the user seeing confusing paths in the failure message. # To fix this, if the CWD changed, always display the full absolute path. # It will be better to just always display paths relative to invocation_dir, but # this requires a lot of plumbing (#6428). - try: + try: abspath = Path(os.getcwd()) != self.config.invocation_params.dir - except OSError: - abspath = True - - return excinfo.getrepr( - funcargs=True, - abspath=abspath, + except OSError: + abspath = True + + return excinfo.getrepr( + funcargs=True, + abspath=abspath, showlocals=self.config.getoption("showlocals", False), - style=style, + style=style, tbfilter=False, # pruned already, or in --fulltrace mode. - truncate_locals=truncate_locals, - ) - + truncate_locals=truncate_locals, + ) + def repr_failure( self, excinfo: ExceptionInfo[BaseException], @@ -414,41 +414,41 @@ class Node(metaclass=NodeMeta): :param excinfo: Exception information for the failure. """ return self._repr_failure_py(excinfo, style) - - + + def get_fslocation_from_item( node: "Node", ) -> Tuple[Union[str, py.path.local], Optional[int]]: """Try to extract the actual location from a node, depending on available attributes: - + * "location": a pair (path, lineno) * "obj": a Python object that the node wraps. - * "fspath": just a path - + * "fspath": just a path + :rtype: A tuple of (str|py.path.local, int) with filename and line number. - """ + """ # See Item.location. location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None) if location is not None: return location[:2] obj = getattr(node, "obj", None) - if obj is not None: - return getfslineno(obj) + if obj is not None: + return getfslineno(obj) return getattr(node, "fspath", "unknown location"), -1 - - -class Collector(Node): + + +class Collector(Node): """Collector instances create children through collect() and thus iteratively build a tree.""" - - class CollectError(Exception): + + class CollectError(Exception): """An error during collection, contains a custom message.""" - + def collect(self) -> Iterable[Union["Item", "Collector"]]: """Return a list of children (items and collectors) for this collection node.""" - raise NotImplementedError("abstract") - + raise NotImplementedError("abstract") + # TODO: This omits the style= parameter which breaks Liskov Substitution. def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException] @@ -460,9 +460,9 @@ class Collector(Node): if isinstance(excinfo.value, self.CollectError) and not self.config.getoption( "fulltrace", False ): - exc = excinfo.value - return str(exc.args[0]) - + exc = excinfo.value + return str(exc.args[0]) + # Respect explicit tbstyle option, but default to "short" # (_repr_failure_py uses "long" with "fulltrace" option always). tbstyle = self.config.getoption("tbstyle", "auto") @@ -472,21 +472,21 @@ class Collector(Node): return self._repr_failure_py(excinfo, style=tbstyle) def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: - if hasattr(self, "fspath"): - traceback = excinfo.traceback - ntraceback = traceback.cut(path=self.fspath) - if ntraceback == traceback: - ntraceback = ntraceback.cut(excludepath=tracebackcutdir) - excinfo.traceback = ntraceback.filter() - - -def _check_initialpaths_for_relpath(session, fspath): - for initial_path in session._initialpaths: - if fspath.common(initial_path) == initial_path: - return fspath.relto(initial_path) - - -class FSCollector(Collector): + if hasattr(self, "fspath"): + traceback = excinfo.traceback + ntraceback = traceback.cut(path=self.fspath) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + excinfo.traceback = ntraceback.filter() + + +def _check_initialpaths_for_relpath(session, fspath): + for initial_path in session._initialpaths: + if fspath.common(initial_path) == initial_path: + return fspath.relto(initial_path) + + +class FSCollector(Collector): def __init__( self, fspath: py.path.local, @@ -495,24 +495,24 @@ class FSCollector(Collector): session: Optional["Session"] = None, nodeid: Optional[str] = None, ) -> None: - name = fspath.basename - if parent is not None: - rel = fspath.relto(parent.fspath) - if rel: - name = rel - name = name.replace(os.sep, SEP) - self.fspath = fspath - - session = session or parent.session - - if nodeid is None: - nodeid = self.fspath.relto(session.config.rootdir) - - if not nodeid: - nodeid = _check_initialpaths_for_relpath(session, fspath) - if nodeid and os.sep != SEP: - nodeid = nodeid.replace(os.sep, SEP) - + name = fspath.basename + if parent is not None: + rel = fspath.relto(parent.fspath) + if rel: + name = rel + name = name.replace(os.sep, SEP) + self.fspath = fspath + + session = session or parent.session + + if nodeid is None: + nodeid = self.fspath.relto(session.config.rootdir) + + if not nodeid: + nodeid = _check_initialpaths_for_relpath(session, fspath) + if nodeid and os.sep != SEP: + nodeid = nodeid.replace(os.sep, SEP) + super().__init__(name, parent, config, session, nodeid=nodeid, fspath=fspath) @classmethod @@ -528,22 +528,22 @@ class FSCollector(Collector): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) - + class File(FSCollector): """Base class for collecting tests from a file. - + :ref:`non-python tests`. """ class Item(Node): """A basic test invocation item. - + Note that for a single function there might be multiple test invocation items. - """ - - nextitem = None - + """ + + nextitem = None + def __init__( self, name, @@ -554,34 +554,34 @@ class Item(Node): ) -> None: super().__init__(name, parent, config, session, nodeid=nodeid) self._report_sections: List[Tuple[str, str, str]] = [] - + #: A list of tuples (name, value) that holds user defined properties #: for this test. self.user_properties: List[Tuple[str, object]] = [] - + def runtest(self) -> None: raise NotImplementedError("runtest must be implemented by Item subclass") 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 stdout and stderr captured output:: - - item.add_report_section("call", "stdout", "report section contents") - - :param str when: - One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. - :param str key: - Name of the section, can be customized at will. Pytest uses ``"stdout"`` and - ``"stderr"`` internally. - :param str content: - The full contents as a string. - """ - if content: - self._report_sections.append((when, key, content)) - + + item.add_report_section("call", "stdout", "report section contents") + + :param str when: + One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``. + :param str key: + Name of the section, can be customized at will. Pytest uses ``"stdout"`` and + ``"stderr"`` internally. + :param str content: + The full contents as a string. + """ + if content: + self._report_sections.append((when, key, content)) + def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]: - return self.fspath, None, "" - + return self.fspath, None, "" + @cached_property def location(self) -> Tuple[str, Optional[int], str]: location = self.reportinfo() diff --git a/contrib/python/pytest/py3/_pytest/nose.py b/contrib/python/pytest/py3/_pytest/nose.py index 684c331272..bb8f99772a 100644 --- a/contrib/python/pytest/py3/_pytest/nose.py +++ b/contrib/python/pytest/py3/_pytest/nose.py @@ -1,39 +1,39 @@ """Run testsuites written for nose.""" -from _pytest import python -from _pytest import unittest -from _pytest.config import hookimpl +from _pytest import python +from _pytest import unittest +from _pytest.config import hookimpl from _pytest.nodes import Item - - -@hookimpl(trylast=True) -def pytest_runtest_setup(item): - if is_potential_nosetest(item): - if not call_optional(item.obj, "setup"): + + +@hookimpl(trylast=True) +def pytest_runtest_setup(item): + if is_potential_nosetest(item): + if not call_optional(item.obj, "setup"): # 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. - item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item) - - -def teardown_nose(item): - if is_potential_nosetest(item): - if not call_optional(item.obj, "teardown"): - call_optional(item.parent.obj, "teardown") - - + item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item) + + +def teardown_nose(item): + if is_potential_nosetest(item): + if not call_optional(item.obj, "teardown"): + call_optional(item.parent.obj, "teardown") + + def is_potential_nosetest(item: Item) -> bool: # Extra check needed since we do not do nose style setup/teardown # on direct unittest style classes. - return isinstance(item, python.Function) and not isinstance( - item, unittest.TestCaseFunction - ) - - -def call_optional(obj, name): - method = getattr(obj, name, None) - isfixture = hasattr(method, "_pytestfixturefunction") - if method is not None and not isfixture and callable(method): - # If there's any problems allow the exception to raise rather than + return isinstance(item, python.Function) and not isinstance( + item, unittest.TestCaseFunction + ) + + +def call_optional(obj, name): + method = getattr(obj, name, None) + isfixture = hasattr(method, "_pytestfixturefunction") + if method is not None and not isfixture and callable(method): + # If there's any problems allow the exception to raise rather than # silently ignoring them. - method() - return True + method() + return True diff --git a/contrib/python/pytest/py3/_pytest/outcomes.py b/contrib/python/pytest/py3/_pytest/outcomes.py index a5411e1d62..8f6203fd7f 100644 --- a/contrib/python/pytest/py3/_pytest/outcomes.py +++ b/contrib/python/pytest/py3/_pytest/outcomes.py @@ -1,15 +1,15 @@ """Exception classes and constants handling test outcomes as well as functions creating them.""" -import sys +import sys from typing import Any from typing import Callable from typing import cast from typing import Optional from typing import Type from typing import TypeVar - + TYPE_CHECKING = False # Avoid circular import through compat. - + if TYPE_CHECKING: from typing import NoReturn from typing_extensions import Protocol @@ -22,10 +22,10 @@ else: Protocol = Generic -class OutcomeException(BaseException): +class OutcomeException(BaseException): """OutcomeException and its subclass instances indicate and contain info about test and collection outcomes.""" - + def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: if msg is not None and not isinstance(msg, str): error_msg = ( # type: ignore[unreachable] @@ -33,53 +33,53 @@ class OutcomeException(BaseException): "Perhaps you meant to use a mark?" ) raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__)) - BaseException.__init__(self, msg) - self.msg = msg - self.pytrace = pytrace - + BaseException.__init__(self, msg) + self.msg = msg + self.pytrace = pytrace + def __repr__(self) -> str: if self.msg is not None: return self.msg return f"<{self.__class__.__name__} instance>" - - __str__ = __repr__ - - -TEST_OUTCOME = (OutcomeException, Exception) - - -class Skipped(OutcomeException): - # XXX hackish: on 3k we fake to live in the builtins - # in order to have Skipped exception printing shorter/nicer - __module__ = "builtins" - + + __str__ = __repr__ + + +TEST_OUTCOME = (OutcomeException, Exception) + + +class Skipped(OutcomeException): + # XXX hackish: on 3k we fake to live in the builtins + # in order to have Skipped exception printing shorter/nicer + __module__ = "builtins" + def __init__( self, msg: Optional[str] = None, pytrace: bool = True, allow_module_level: bool = False, ) -> None: - OutcomeException.__init__(self, msg=msg, pytrace=pytrace) - self.allow_module_level = allow_module_level - - -class Failed(OutcomeException): + OutcomeException.__init__(self, msg=msg, pytrace=pytrace) + self.allow_module_level = allow_module_level + + +class Failed(OutcomeException): """Raised from an explicit call to pytest.fail().""" - - __module__ = "builtins" - - + + __module__ = "builtins" + + class Exit(Exception): """Raised for immediate program exits (no tracebacks/summaries).""" - + def __init__( self, msg: str = "unknown reason", returncode: Optional[int] = None ) -> None: - self.msg = msg - self.returncode = returncode + self.msg = msg + self.returncode = returncode super().__init__(msg) - - + + # Elaborate hack to work around https://github.com/python/mypy/issues/2087. # Ideally would just be `exit.Exception = Exit` etc. @@ -102,82 +102,82 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E # Exposed helper methods. - - + + @_with_exception(Exit) def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn": """Exit testing process. - + :param str msg: Message to display upon exit. :param int returncode: Return code to be used when exiting pytest. - """ - __tracebackhide__ = True - raise Exit(msg, returncode) - - + """ + __tracebackhide__ = True + raise Exit(msg, returncode) + + @_with_exception(Skipped) def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn": """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 be called in doctests as well. - + :param bool allow_module_level: 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 skipped under certain conditions like mismatching platforms or dependencies. Similarly, use the ``# doctest: +SKIP`` directive (see `doctest.SKIP <https://docs.python.org/3/library/doctest.html#doctest.SKIP>`_) to skip a doctest statically. - """ - __tracebackhide__ = True - raise Skipped(msg=msg, allow_module_level=allow_module_level) - - + """ + __tracebackhide__ = True + raise Skipped(msg=msg, allow_module_level=allow_module_level) + + @_with_exception(Failed) def fail(msg: str = "", pytrace: bool = True) -> "NoReturn": """Explicitly fail an executing test with the given message. - + :param str msg: 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. - """ - __tracebackhide__ = True - raise Failed(msg=msg, pytrace=pytrace) - - + python traceback will be reported. + """ + __tracebackhide__ = True + raise Failed(msg=msg, pytrace=pytrace) + + class XFailed(Failed): """Raised from an explicit call to pytest.xfail().""" - - + + @_with_exception(XFailed) def xfail(reason: str = "") -> "NoReturn": """Imperatively xfail an executing test or setup function with the given reason. - - This function should be called only during testing (setup, call or teardown). - - .. note:: + + This function should be called only during testing (setup, call or teardown). + + .. note:: It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be xfailed under certain conditions like known bugs or missing features. - """ - __tracebackhide__ = True - raise XFailed(reason) - - + """ + __tracebackhide__ = True + raise XFailed(reason) + + def importorskip( modname: str, minversion: Optional[str] = None, reason: Optional[str] = None ) -> Any: """Import and return the requested module ``modname``, or skip the current test if the module cannot be imported. - + :param str modname: The name of the module to import. :param str minversion: @@ -186,42 +186,42 @@ def importorskip( :param str reason: If given, this reason is shown as the message when the module cannot be imported. - + :returns: The imported module. This should be assigned to its canonical name. Example:: docutils = pytest.importorskip("docutils") - """ - import warnings - - __tracebackhide__ = True - compile(modname, "", "eval") # to catch syntaxerrors - - with warnings.catch_warnings(): + """ + import warnings + + __tracebackhide__ = True + compile(modname, "", "eval") # to catch syntaxerrors + + with warnings.catch_warnings(): # 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. - warnings.simplefilter("ignore") - try: - __import__(modname) + warnings.simplefilter("ignore") + try: + __import__(modname) except ImportError as exc: if reason is None: reason = f"could not import {modname!r}: {exc}" raise Skipped(reason, allow_module_level=True) from None - mod = sys.modules[modname] - if minversion is None: - return mod - verattr = getattr(mod, "__version__", None) - if minversion is not None: + mod = sys.modules[modname] + if minversion is None: + return mod + verattr = getattr(mod, "__version__", None) + if minversion is not None: # Imported lazily to improve start-up time. from packaging.version import Version if verattr is None or Version(verattr) < Version(minversion): - raise Skipped( - "module %r has __version__ %r, required is: %r" - % (modname, verattr, minversion), - allow_module_level=True, - ) - return mod + raise Skipped( + "module %r has __version__ %r, required is: %r" + % (modname, verattr, minversion), + allow_module_level=True, + ) + return mod diff --git a/contrib/python/pytest/py3/_pytest/pastebin.py b/contrib/python/pytest/py3/_pytest/pastebin.py index 22b6151fb2..131873c174 100644 --- a/contrib/python/pytest/py3/_pytest/pastebin.py +++ b/contrib/python/pytest/py3/_pytest/pastebin.py @@ -1,54 +1,54 @@ """Submit failure or test session information to a pastebin service.""" -import tempfile +import tempfile from io import StringIO from typing import IO from typing import Union - -import pytest + +import pytest from _pytest.config import Config from _pytest.config import create_terminal_writer from _pytest.config.argparsing import Parser from _pytest.store import StoreKey from _pytest.terminal import TerminalReporter - - + + pastebinfile_key = StoreKey[IO[bytes]]() def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("terminal reporting") - group._addoption( - "--pastebin", - metavar="mode", - action="store", - dest="pastebin", - default=None, - choices=["failed", "all"], - help="send failed|all info to bpaste.net pastebin service.", - ) - - -@pytest.hookimpl(trylast=True) + group = parser.getgroup("terminal reporting") + group._addoption( + "--pastebin", + metavar="mode", + action="store", + dest="pastebin", + default=None, + choices=["failed", "all"], + help="send failed|all info to bpaste.net pastebin service.", + ) + + +@pytest.hookimpl(trylast=True) def pytest_configure(config: Config) -> None: - if config.option.pastebin == "all": - tr = config.pluginmanager.getplugin("terminalreporter") + if config.option.pastebin == "all": + tr = config.pluginmanager.getplugin("terminalreporter") # If no terminal reporter plugin is present, nothing we can do here; # this can happen when this function executes in a worker node # 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. config._store[pastebinfile_key] = tempfile.TemporaryFile("w+b") - oldwrite = tr._tw.write - - def tee_write(s, **kwargs): - oldwrite(s, **kwargs) + oldwrite = tr._tw.write + + def tee_write(s, **kwargs): + oldwrite(s, **kwargs) if isinstance(s, str): - s = s.encode("utf-8") + s = s.encode("utf-8") config._store[pastebinfile_key].write(s) - - tr._tw.write = tee_write - - + + tr._tw.write = tee_write + + def pytest_unconfigure(config: Config) -> None: if pastebinfile_key in config._store: pastebinfile = config._store[pastebinfile_key] @@ -58,26 +58,26 @@ def pytest_unconfigure(config: Config) -> None: pastebinfile.close() del config._store[pastebinfile_key] # Undo our patching in the terminal reporter. - tr = config.pluginmanager.getplugin("terminalreporter") - del tr._tw.__dict__["write"] + tr = config.pluginmanager.getplugin("terminalreporter") + del tr._tw.__dict__["write"] # Write summary. - tr.write_sep("=", "Sending information to Paste Service") - pastebinurl = create_new_paste(sessionlog) - tr.write_line("pastebin session-log: %s\n" % pastebinurl) - - + tr.write_sep("=", "Sending information to Paste Service") + pastebinurl = create_new_paste(sessionlog) + tr.write_line("pastebin session-log: %s\n" % pastebinurl) + + def create_new_paste(contents: Union[str, bytes]) -> str: """Create a new paste using the bpaste.net service. - + :contents: Paste contents string. :returns: URL to the pasted contents, or an error message. - """ - import re + """ + import re from urllib.request import urlopen from urllib.parse import urlencode - + params = {"code": contents, "lexer": "text", "expiry": "1week"} - url = "https://bpaste.net" + url = "https://bpaste.net" try: response: str = ( urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") @@ -85,26 +85,26 @@ def create_new_paste(contents: Union[str, bytes]) -> str: except OSError as exc_info: # urllib errors return "bad response: %s" % exc_info m = re.search(r'href="/raw/(\w+)"', response) - if m: + if m: return "{}/show/{}".format(url, m.group(1)) - else: + else: return "bad response: invalid format ('" + response + "')" - - + + def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: - if terminalreporter.config.option.pastebin != "failed": - return + if terminalreporter.config.option.pastebin != "failed": + return if "failed" in terminalreporter.stats: - terminalreporter.write_sep("=", "Sending information to Paste Service") + terminalreporter.write_sep("=", "Sending information to Paste Service") for rep in terminalreporter.stats["failed"]: - try: - msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc - except AttributeError: + try: + msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc + except AttributeError: msg = terminalreporter._getfailureheadline(rep) file = StringIO() tw = create_terminal_writer(terminalreporter.config, file) - rep.toterminal(tw) + rep.toterminal(tw) s = file.getvalue() - assert len(s) - pastebinurl = create_new_paste(s) + assert len(s) + pastebinurl = create_new_paste(s) terminalreporter.write_line(f"{msg} --> {pastebinurl}") diff --git a/contrib/python/pytest/py3/_pytest/pathlib.py b/contrib/python/pytest/py3/_pytest/pathlib.py index 02ba612d32..7d9269a185 100644 --- a/contrib/python/pytest/py3/_pytest/pathlib.py +++ b/contrib/python/pytest/py3/_pytest/pathlib.py @@ -1,12 +1,12 @@ -import atexit +import atexit import contextlib -import fnmatch +import fnmatch import importlib.util -import itertools -import os -import shutil -import sys -import uuid +import itertools +import os +import shutil +import sys +import uuid import warnings from enum import Enum from errno import EBADF @@ -14,13 +14,13 @@ from errno import ELOOP from errno import ENOENT from errno import ENOTDIR from functools import partial -from os.path import expanduser -from os.path import expandvars -from os.path import isabs -from os.path import sep +from os.path import expanduser +from os.path import expandvars +from os.path import isabs +from os.path import sep from pathlib import Path from pathlib import PurePath -from posixpath import sep as posix_sep +from posixpath import sep as posix_sep from types import ModuleType from typing import Callable from typing import Iterable @@ -29,29 +29,29 @@ from typing import Optional from typing import Set from typing import TypeVar from typing import Union - + import py from _pytest.compat import assert_never from _pytest.outcomes import skip from _pytest.warning_types import PytestWarning - + LOCK_TIMEOUT = 60 * 60 * 24 * 3 - - + + _AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) - + # The following function, variables and comments were # copied from cpython 3.9 Lib/pathlib.py file. - + # EBADF - guard against macOS `stat` throwing EBADF _IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP) - + _IGNORED_WINERRORS = ( 21, # ERROR_NOT_READY - drive exists but is not accessible 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself ) - + def _ignore_error(exception): return ( @@ -66,11 +66,11 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: """Handle known read-only errors during rmtree. - + The returned value is used only by our own tests. """ exctype, excvalue = exc[:2] - + # 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 if isinstance(excvalue, FileNotFoundError): @@ -155,147 +155,147 @@ def rm_rf(path: Path) -> None: def find_prefixed(root: Path, prefix: str) -> Iterator[Path]: """Find all elements in root that begin with the prefix, case insensitive.""" - l_prefix = prefix.lower() - for x in root.iterdir(): - if x.name.lower().startswith(l_prefix): - yield x - - + l_prefix = prefix.lower() + for x in root.iterdir(): + if x.name.lower().startswith(l_prefix): + yield x + + 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. - """ - p_len = len(prefix) - for p in iter: - yield p.name[p_len:] - - + """ + p_len = len(prefix) + for p in iter: + yield p.name[p_len:] + + def find_suffixes(root: Path, prefix: str) -> Iterator[str]: """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: """Parse number path suffixes, returns -1 on error.""" - try: - return int(maybe_num) - except ValueError: - return -1 - - + try: + return int(maybe_num) + except ValueError: + return -1 + + def _force_symlink( root: Path, target: Union[str, PurePath], link_to: Union[str, Path] ) -> None: """Helper to create the current symlink. - + It's full of race conditions that are reasonably OK to ignore for the context of best effort linking to the latest test run. - + The presumption being that in case of much parallelism the inaccuracy is going to be acceptable. - """ - current_symlink = root.joinpath(target) - try: - current_symlink.unlink() - except OSError: - pass - try: - current_symlink.symlink_to(link_to) - except Exception: - pass - - + """ + current_symlink = root.joinpath(target) + try: + current_symlink.unlink() + except OSError: + pass + try: + current_symlink.symlink_to(link_to) + except Exception: + pass + + def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: """Create a directory with an increased number as suffix for the given prefix.""" - for i in range(10): - # try up to 10 times to create the folder + for i in range(10): + # try up to 10 times to create the folder max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) - new_number = max_existing + 1 + new_number = max_existing + 1 new_path = root.joinpath(f"{prefix}{new_number}") - try: + try: new_path.mkdir(mode=mode) - except Exception: - pass - else: - _force_symlink(root, prefix + "current", new_path) - return new_path - else: + except Exception: + pass + else: + _force_symlink(root, prefix + "current", new_path) + return new_path + else: raise OSError( - "could not create numbered dir with prefix " - "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) - ) - - + "could not create numbered dir with prefix " + "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) + ) + + def create_cleanup_lock(p: Path) -> Path: """Create a lock to prevent premature folder cleanup.""" - lock_path = get_lock_path(p) - try: - fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + lock_path = get_lock_path(p) + try: + fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) except FileExistsError as e: raise OSError(f"cannot create lockfile in {p}") from e - else: - pid = os.getpid() + else: + pid = os.getpid() spid = str(pid).encode() - os.write(fd, spid) - os.close(fd) - if not lock_path.is_file(): + os.write(fd, spid) + os.close(fd) + if not lock_path.is_file(): raise OSError("lock path got renamed after successful creation") - return lock_path - - + return lock_path + + def register_cleanup_lock_removal(lock_path: Path, register=atexit.register): """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: - current_pid = os.getpid() - if current_pid != original_pid: - # fork - return - try: - lock_path.unlink() + current_pid = os.getpid() + if current_pid != original_pid: + # fork + return + try: + lock_path.unlink() except OSError: - pass - - return register(cleanup_on_exit) - - + pass + + return register(cleanup_on_exit) + + def maybe_delete_a_numbered_dir(path: Path) -> None: """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) - lock_path = None - try: - lock_path = create_cleanup_lock(path) - parent = path.parent - + lock_path = None + try: + lock_path = create_cleanup_lock(path) + parent = path.parent + garbage = parent.joinpath(f"garbage-{uuid.uuid4()}") - path.rename(garbage) + path.rename(garbage) rm_rf(garbage) except OSError: - # known races: - # * other process did a cleanup at the same time - # * deletable folder was found - # * process cwd (Windows) - return - finally: + # known races: + # * other process did a cleanup at the same time + # * deletable folder was found + # * process cwd (Windows) + return + finally: # If we created the lock, ensure we remove it even if we failed # to properly remove the numbered dir. - if lock_path is not None: - try: - lock_path.unlink() + if lock_path is not None: + try: + lock_path.unlink() except OSError: - pass - - + pass + + def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: """Check if `path` is deletable based on whether the lock file is expired.""" - if path.is_symlink(): - return False - lock = get_lock_path(path) - try: + if path.is_symlink(): + return False + lock = get_lock_path(path) + try: if not lock.is_file(): return True except OSError: @@ -303,11 +303,11 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> # we don't have access to the entire directory (#7491). return False try: - lock_time = lock.stat().st_mtime - except Exception: - return False - else: - if lock_time < consider_lock_dead_if_created_before: + lock_time = lock.stat().st_mtime + except Exception: + return False + else: + if lock_time < consider_lock_dead_if_created_before: # 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; # - FileNotFoundError, in case another pytest process got here first; @@ -316,50 +316,50 @@ def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> lock.unlink() return True return False - - + + def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None: """Try to cleanup a folder if we can ensure it's deletable.""" - if ensure_deletable(path, consider_lock_dead_if_created_before): - maybe_delete_a_numbered_dir(path) - - + if ensure_deletable(path, consider_lock_dead_if_created_before): + maybe_delete_a_numbered_dir(path) + + def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[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_delete = max_existing - keep - paths = find_prefixed(root, prefix) - paths, paths2 = itertools.tee(paths) - numbers = map(parse_num, extract_suffixes(paths2, prefix)) - for path, number in zip(paths, numbers): - if number <= max_delete: - yield path - - + max_delete = max_existing - keep + paths = find_prefixed(root, prefix) + paths, paths2 = itertools.tee(paths) + numbers = map(parse_num, extract_suffixes(paths2, prefix)) + for path, number in zip(paths, numbers): + if number <= max_delete: + yield path + + def cleanup_numbered_dir( root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float ) -> None: """Cleanup for lock driven numbered directories.""" - for path in cleanup_candidates(root, prefix, keep): - try_cleanup(path, consider_lock_dead_if_created_before) - for path in root.glob("garbage-*"): - try_cleanup(path, consider_lock_dead_if_created_before) - - + for path in cleanup_candidates(root, prefix, keep): + try_cleanup(path, consider_lock_dead_if_created_before) + for path in root.glob("garbage-*"): + try_cleanup(path, consider_lock_dead_if_created_before) + + def make_numbered_dir_with_cleanup( root: Path, prefix: str, keep: int, lock_timeout: float, mode: int, ) -> Path: """Create a numbered dir with a cleanup lock and remove old ones.""" - e = None - for i in range(10): - try: + e = None + for i in range(10): + try: p = make_numbered_dir(root, prefix, mode) - lock_path = create_cleanup_lock(p) - register_cleanup_lock_removal(lock_path) - except Exception as exc: - e = exc - else: - consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout + lock_path = create_cleanup_lock(p) + register_cleanup_lock_removal(lock_path) + except Exception as exc: + e = exc + else: + consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout # Register a cleanup for program exit atexit.register( cleanup_numbered_dir, @@ -367,60 +367,60 @@ def make_numbered_dir_with_cleanup( prefix, keep, consider_lock_dead_if_created_before, - ) - return p - assert e is not None - raise e - - + ) + return p + assert e is not None + raise e + + def resolve_from_str(input: str, rootpath: Path) -> Path: - input = expanduser(input) - input = expandvars(input) - if isabs(input): - return Path(input) - else: + input = expanduser(input) + input = expandvars(input) + if isabs(input): + return Path(input) + else: return rootpath.joinpath(input) - - + + def fnmatch_ex(pattern: str, path) -> bool: """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 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 PurePath.match(). - + This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according this logic. - - References: - * https://bugs.python.org/issue29249 - * https://bugs.python.org/issue34731 - """ - path = PurePath(path) - iswin32 = sys.platform.startswith("win") - - if iswin32 and sep not in pattern and posix_sep in pattern: - # Running on Windows, the pattern has no Windows path separators, - # and the pattern has one or more Posix path separators. Replace - # the Posix path separators with the Windows path separator. - pattern = pattern.replace(posix_sep, sep) - - if sep not in pattern: - name = path.name - else: + + References: + * https://bugs.python.org/issue29249 + * https://bugs.python.org/issue34731 + """ + path = PurePath(path) + iswin32 = sys.platform.startswith("win") + + if iswin32 and sep not in pattern and posix_sep in pattern: + # Running on Windows, the pattern has no Windows path separators, + # and the pattern has one or more Posix path separators. Replace + # the Posix path separators with the Windows path separator. + pattern = pattern.replace(posix_sep, sep) + + if sep not in pattern: + name = path.name + else: name = str(path) if path.is_absolute() and not os.path.isabs(pattern): pattern = f"*{os.sep}{pattern}" - return fnmatch.fnmatch(name, pattern) - - + return fnmatch.fnmatch(name, pattern) + + def parts(s: str) -> Set[str]: - parts = s.split(sep) - return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} + parts = s.split(sep) + return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} def symlink_or_skip(src, dst, **kwargs): diff --git a/contrib/python/pytest/py3/_pytest/pytester.py b/contrib/python/pytest/py3/_pytest/pytester.py index 5c67e559fc..31259d1bdc 100644 --- a/contrib/python/pytest/py3/_pytest/pytester.py +++ b/contrib/python/pytest/py3/_pytest/pytester.py @@ -4,16 +4,16 @@ PYTEST_DONT_REWRITE """ import collections.abc import contextlib -import gc +import gc import importlib -import os -import platform -import re +import os +import platform +import re import shutil -import subprocess -import sys -import traceback -from fnmatch import fnmatch +import subprocess +import sys +import traceback +from fnmatch import fnmatch from io import StringIO from pathlib import Path from typing import Any @@ -30,15 +30,15 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union -from weakref import WeakKeyDictionary - +from weakref import WeakKeyDictionary + import attr -import py +import py from iniconfig import IniConfig from iniconfig import SectionWrapper - + from _pytest import timing -from _pytest._code import Source +from _pytest._code import Source from _pytest.capture import _get_multicapture from _pytest.compat import final from _pytest.config import _PluggyPlugin @@ -51,7 +51,7 @@ from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest -from _pytest.main import Session +from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector from _pytest.nodes import Item @@ -63,7 +63,7 @@ from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestWarning - + if TYPE_CHECKING: from typing_extensions import Literal @@ -73,48 +73,48 @@ if TYPE_CHECKING: pytest_plugins = ["pytester_assertions"] -IGNORE_PAM = [ # filenames added when obtaining details about the current user +IGNORE_PAM = [ # filenames added when obtaining details about the current user "/var/lib/sss/mc/passwd" -] - - +] + + def pytest_addoption(parser: Parser) -> None: - parser.addoption( - "--lsof", - action="store_true", - dest="lsof", - default=False, - help="run FD checks if lsof is available", - ) - - parser.addoption( - "--runpytest", - default="inprocess", - dest="runpytest", - choices=("inprocess", "subprocess"), - help=( - "run pytest sub runs in tests using an 'inprocess' " - "or 'subprocess' (python -m main) method" - ), - ) - - parser.addini( - "pytester_example_dir", help="directory to take the pytester example files from" - ) - - + parser.addoption( + "--lsof", + action="store_true", + dest="lsof", + default=False, + help="run FD checks if lsof is available", + ) + + parser.addoption( + "--runpytest", + default="inprocess", + dest="runpytest", + choices=("inprocess", "subprocess"), + help=( + "run pytest sub runs in tests using an 'inprocess' " + "or 'subprocess' (python -m main) method" + ), + ) + + parser.addini( + "pytester_example_dir", help="directory to take the pytester example files from" + ) + + def pytest_configure(config: Config) -> None: - if config.getvalue("lsof"): - checker = LsofFdLeakChecker() - if checker.matching_platform(): - config.pluginmanager.register(checker) - + if config.getvalue("lsof"): + checker = LsofFdLeakChecker() + if checker.matching_platform(): + config.pluginmanager.register(checker) + config.addinivalue_line( "markers", "pytester_example_path(*path_segments): join the given path " "segments to `pytester_example_dir` for this test.", ) - + class LsofFdLeakChecker: def get_open_files(self) -> List[Tuple[str, str]]: @@ -125,48 +125,48 @@ class LsofFdLeakChecker: check=True, universal_newlines=True, ).stdout - + def isopen(line: str) -> bool: - return line.startswith("f") and ( - "deleted" not in line - and "mem" not in line - and "txt" not in line - and "cwd" not in line - ) - - open_files = [] - - for line in out.split("\n"): - if isopen(line): - fields = line.split("\0") - fd = fields[0][1:] - filename = fields[1][1:] - if filename in IGNORE_PAM: - continue - if filename.startswith("/"): - open_files.append((fd, filename)) - - return open_files - + return line.startswith("f") and ( + "deleted" not in line + and "mem" not in line + and "txt" not in line + and "cwd" not in line + ) + + open_files = [] + + for line in out.split("\n"): + if isopen(line): + fields = line.split("\0") + fd = fields[0][1:] + filename = fields[1][1:] + if filename in IGNORE_PAM: + continue + if filename.startswith("/"): + open_files.append((fd, filename)) + + return open_files + def matching_platform(self) -> bool: - try: + try: subprocess.run(("lsof", "-v"), check=True) except (OSError, subprocess.CalledProcessError): - return False - else: - return True - + return False + else: + return True + @hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: - lines1 = self.get_open_files() - yield - if hasattr(sys, "pypy_version_info"): - gc.collect() - lines2 = self.get_open_files() - - new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} - leaked_files = [t for t in lines2 if t[0] in new_fds] - if leaked_files: + lines1 = self.get_open_files() + yield + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() + + new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: error = [ "***** %s FD leakage detected" % len(leaked_files), *(str(f) for f in leaked_files), @@ -179,116 +179,116 @@ class LsofFdLeakChecker: "See issue #2366", ] item.warn(PytestWarning("\n".join(error))) - - -# used at least by pytest-xdist plugin - - + + +# used at least by pytest-xdist plugin + + @fixture def _pytest(request: FixtureRequest) -> "PytestArg": - """Return a helper which offers a gethookrecorder(hook) method which - returns a HookRecorder instance which helps to make assertions about called + """Return a helper which offers a gethookrecorder(hook) method which + returns a HookRecorder instance which helps to make assertions about called hooks.""" - return PytestArg(request) - - + return PytestArg(request) + + class PytestArg: def __init__(self, request: FixtureRequest) -> None: self._request = request - + def gethookrecorder(self, hook) -> "HookRecorder": - hookrecorder = HookRecorder(hook._pm) + hookrecorder = HookRecorder(hook._pm) self._request.addfinalizer(hookrecorder.finish_recording) - return hookrecorder - - + return hookrecorder + + def get_public_names(values: Iterable[str]) -> List[str]: - """Only return names from iterator values without a leading underscore.""" - return [x for x in values if x[0] != "_"] - - + """Only return names from iterator values without a leading underscore.""" + return [x for x in values if x[0] != "_"] + + class ParsedCall: def __init__(self, name: str, kwargs) -> None: - self.__dict__.update(kwargs) - self._name = name - + self.__dict__.update(kwargs) + self._name = name + def __repr__(self) -> str: - d = self.__dict__.copy() - del d["_name"] + d = self.__dict__.copy() + del d["_name"] return f"<ParsedCall {self._name!r}(**{d!r})>" - + if TYPE_CHECKING: # The class has undetermined attributes, this tells mypy about it. def __getattr__(self, key: str): ... - + class HookRecorder: - """Record all hooks called in a plugin manager. - - This wraps all the hook calls in the plugin manager, recording each call - before propagating the normal calls. - """ - + """Record all hooks called in a plugin manager. + + This wraps all the hook calls in the plugin manager, recording each call + before propagating the normal calls. + """ + def __init__(self, pluginmanager: PytestPluginManager) -> None: - self._pluginmanager = pluginmanager + self._pluginmanager = pluginmanager self.calls: List[ParsedCall] = [] self.ret: Optional[Union[int, ExitCode]] = None - + def before(hook_name: str, hook_impls, kwargs) -> None: - self.calls.append(ParsedCall(hook_name, kwargs)) - + self.calls.append(ParsedCall(hook_name, kwargs)) + def after(outcome, hook_name: str, hook_impls, kwargs) -> None: - pass - - self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) - + pass + + self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) + def finish_recording(self) -> None: - self._undo_wrapping() - + self._undo_wrapping() + def getcalls(self, names: Union[str, Iterable[str]]) -> List[ParsedCall]: - if isinstance(names, str): - names = names.split() - return [call for call in self.calls if call._name in names] - + if isinstance(names, str): + names = names.split() + return [call for call in self.calls if call._name in names] + def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None: - __tracebackhide__ = True - i = 0 - entries = list(entries) - backlocals = sys._getframe(1).f_locals - while entries: - name, check = entries.pop(0) - for ind, call in enumerate(self.calls[i:]): - if call._name == name: - print("NAMEMATCH", name, call) - if eval(check, backlocals, call.__dict__): - print("CHECKERMATCH", repr(check), "->", call) - else: - print("NOCHECKERMATCH", repr(check), "-", call) - continue - i += ind + 1 - break - print("NONAMEMATCH", name, "with", call) - else: + __tracebackhide__ = True + i = 0 + entries = list(entries) + backlocals = sys._getframe(1).f_locals + while entries: + name, check = entries.pop(0) + for ind, call in enumerate(self.calls[i:]): + if call._name == name: + print("NAMEMATCH", name, call) + if eval(check, backlocals, call.__dict__): + print("CHECKERMATCH", repr(check), "->", call) + else: + print("NOCHECKERMATCH", repr(check), "-", call) + continue + i += ind + 1 + break + print("NONAMEMATCH", name, "with", call) + else: fail(f"could not find {name!r} check {check!r}") - + def popcall(self, name: str) -> ParsedCall: - __tracebackhide__ = True - for i, call in enumerate(self.calls): - if call._name == name: - del self.calls[i] - return call + __tracebackhide__ = True + for i, call in enumerate(self.calls): + if call._name == name: + del self.calls[i] + return call lines = [f"could not find call {name!r}, in:"] - lines.extend([" %s" % x for x in self.calls]) + lines.extend([" %s" % x for x in self.calls]) fail("\n".join(lines)) - + def getcall(self, name: str) -> ParsedCall: - values = self.getcalls(name) - assert len(values) == 1, (name, values) - return values[0] - - # functionality for test reports - + values = self.getcalls(name) + assert len(values) == 1, (name, values) + return values[0] + + # functionality for test reports + @overload def getreports( self, names: "Literal['pytest_collectreport']", @@ -318,10 +318,10 @@ class HookRecorder: "pytest_runtest_logreport", ), ) -> Sequence[Union[CollectReport, TestReport]]: - return [x.report for x in self.getcalls(names)] - - def matchreport( - self, + return [x.report for x in self.getcalls(names)] + + def matchreport( + self, inamepart: str = "", names: Union[str, Iterable[str]] = ( "pytest_runtest_logreport", @@ -330,28 +330,28 @@ class HookRecorder: when: Optional[str] = None, ) -> Union[CollectReport, TestReport]: """Return a testreport whose dotted import path matches.""" - values = [] - for rep in self.getreports(names=names): + values = [] + for rep in self.getreports(names=names): if not when and rep.when != "call" and rep.passed: # setup/teardown passing reports - let's ignore those - continue + continue if when and rep.when != when: continue - if not inamepart or inamepart in rep.nodeid.split("::"): - values.append(rep) - if not values: - raise ValueError( - "could not find test report matching %r: " - "no test reports at all!" % (inamepart,) - ) - if len(values) > 1: - raise ValueError( + if not inamepart or inamepart in rep.nodeid.split("::"): + values.append(rep) + if not values: + raise ValueError( + "could not find test report matching %r: " + "no test reports at all!" % (inamepart,) + ) + if len(values) > 1: + raise ValueError( "found 2 or more testreports matching {!r}: {}".format( inamepart, values ) - ) - return values[0] - + ) + return values[0] + @overload def getfailures( self, names: "Literal['pytest_collectreport']", @@ -381,11 +381,11 @@ class HookRecorder: "pytest_runtest_logreport", ), ) -> Sequence[Union[CollectReport, TestReport]]: - return [rep for rep in self.getreports(names) if rep.failed] - + return [rep for rep in self.getreports(names) if rep.failed] + def getfailedcollections(self) -> Sequence[CollectReport]: - return self.getfailures("pytest_collectreport") - + return self.getfailures("pytest_collectreport") + def listoutcomes( self, ) -> Tuple[ @@ -393,46 +393,46 @@ class HookRecorder: Sequence[Union[CollectReport, TestReport]], Sequence[Union[CollectReport, TestReport]], ]: - passed = [] - skipped = [] - failed = [] + passed = [] + skipped = [] + failed = [] for rep in self.getreports( ("pytest_collectreport", "pytest_runtest_logreport") ): - if rep.passed: + if rep.passed: if rep.when == "call": assert isinstance(rep, TestReport) - passed.append(rep) - elif rep.skipped: - skipped.append(rep) + passed.append(rep) + elif rep.skipped: + skipped.append(rep) else: assert rep.failed, f"Unexpected outcome: {rep!r}" - failed.append(rep) - return passed, skipped, failed - + failed.append(rep) + return passed, skipped, failed + def countoutcomes(self) -> List[int]: - return [len(x) for x in self.listoutcomes()] - + return [len(x) for x in self.listoutcomes()] + def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: __tracebackhide__ = True from _pytest.pytester_assertions import assertoutcome - + outcomes = self.listoutcomes() assertoutcome( outcomes, passed=passed, skipped=skipped, failed=failed, ) def clear(self) -> None: - self.calls[:] = [] - - + self.calls[:] = [] + + @fixture def linecomp() -> "LineComp": """A :class: `LineComp` instance for checking that an input linearly contains a sequence of strings.""" - return LineComp() - - + return LineComp() + + @fixture(name="LineMatcher") def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: """A reference to the :class: `LineMatcher`. @@ -440,18 +440,18 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: 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. """ - return LineMatcher - - + return LineMatcher + + @fixture def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": """ Facilities to write tests/configuration files, execute pytest in isolation, and match against expected output, perfect for black-box testing of pytest plugins. - + It attempts to isolate the test run from external factors as much as possible, modifying the current working directory to ``path`` and environment variables during initialization. - + It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` fixture but provides methods which aid in testing pytest itself. """ @@ -491,11 +491,11 @@ def _config_for_test() -> Generator[Config, None, None]: 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". rex_outcome = re.compile(r"(\d+) (\w+)") - - + + class RunResult: """The result of running a command.""" - + def __init__( self, ret: Union[int, ExitCode], @@ -508,21 +508,21 @@ class RunResult: """The return value.""" except ValueError: self.ret = ret - self.outlines = outlines + self.outlines = outlines """List of lines captured from stdout.""" - self.errlines = errlines + self.errlines = errlines """List of lines captured from stderr.""" - self.stdout = LineMatcher(outlines) + self.stdout = LineMatcher(outlines) """:class:`LineMatcher` of stdout. Use e.g. :func:`str(stdout) <LineMatcher.__str__()>` to reconstruct stdout, or the commonly used :func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method. """ - self.stderr = LineMatcher(errlines) + self.stderr = LineMatcher(errlines) """:class:`LineMatcher` of stderr.""" - self.duration = duration + self.duration = duration """Duration in seconds.""" - + def __repr__(self) -> str: return ( "<RunResult ret=%s len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>" @@ -531,14 +531,14 @@ class RunResult: def parseoutcomes(self) -> Dict[str, int]: """Return a dictionary of outcome noun -> count from parsing the terminal - output that the test process produced. - + output that the test process produced. + The returned nouns will always be in plural form:: ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ==== Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``. - """ + """ return self.parse_summary_nouns(self.outlines) @classmethod @@ -553,19 +553,19 @@ class RunResult: """ for line in reversed(lines): if rex_session_duration.search(line): - outcomes = rex_outcome.findall(line) + outcomes = rex_outcome.findall(line) ret = {noun: int(count) for (count, noun) in outcomes} break else: raise ValueError("Pytest terminal summary report not found") - + to_plural = { "warning": "warnings", "error": "errors", } return {to_plural.get(k, k): v for k, v in ret.items()} - def assert_outcomes( + def assert_outcomes( self, passed: int = 0, skipped: int = 0, @@ -574,11 +574,11 @@ class RunResult: xpassed: int = 0, xfailed: int = 0, ) -> 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.""" __tracebackhide__ = True from _pytest.pytester_assertions import assert_outcomes - + outcomes = self.parseoutcomes() assert_outcomes( outcomes, @@ -589,65 +589,65 @@ class RunResult: xpassed=xpassed, xfailed=xfailed, ) - - + + class CwdSnapshot: def __init__(self) -> None: - self.__saved = os.getcwd() - + self.__saved = os.getcwd() + def restore(self) -> None: - os.chdir(self.__saved) - - + os.chdir(self.__saved) + + class SysModulesSnapshot: def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None: - self.__preserve = preserve - self.__saved = dict(sys.modules) - + self.__preserve = preserve + self.__saved = dict(sys.modules) + def restore(self) -> None: - if self.__preserve: - self.__saved.update( - (k, m) for k, m in sys.modules.items() if self.__preserve(k) - ) - sys.modules.clear() - sys.modules.update(self.__saved) - - + if self.__preserve: + self.__saved.update( + (k, m) for k, m in sys.modules.items() if self.__preserve(k) + ) + sys.modules.clear() + sys.modules.update(self.__saved) + + class SysPathsSnapshot: def __init__(self) -> None: - self.__saved = list(sys.path), list(sys.meta_path) - + self.__saved = list(sys.path), list(sys.meta_path) + def restore(self) -> None: - sys.path[:], sys.meta_path[:] = self.__saved - - + sys.path[:], sys.meta_path[:] = self.__saved + + @final class Pytester: """ Facilities to write tests/configuration files, execute pytest in isolation, and match against expected output, perfect for black-box testing of pytest plugins. - + It attempts to isolate the test run from external factors as much as possible, modifying the current working directory to ``path`` and environment variables during initialization. - - Attributes: - + + Attributes: + :ivar Path path: temporary directory path used to create files/run tests from, etc. - + :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 - 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. - """ - + :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 + the method using them so refer to them for details. + """ + __test__ = False CLOSE_STDIN = object - class TimeoutExpired(Exception): - pass - + class TimeoutExpired(Exception): + pass + def __init__( self, request: FixtureRequest, @@ -667,14 +667,14 @@ class Pytester: self._name = name self._path: Path = tmp_path_factory.mktemp(name, numbered=True) self.plugins: List[Union[str, _PluggyPlugin]] = [] - self._cwd_snapshot = CwdSnapshot() - self._sys_path_snapshot = SysPathsSnapshot() - self._sys_modules_snapshot = self.__take_sys_modules_snapshot() - self.chdir() + self._cwd_snapshot = CwdSnapshot() + self._sys_path_snapshot = SysPathsSnapshot() + self._sys_modules_snapshot = self.__take_sys_modules_snapshot() + self.chdir() self._request.addfinalizer(self._finalize) self._method = self._request.config.getoption("--runpytest") self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) - + self._monkeypatch = mp = MonkeyPatch() mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) # Ensure no unexpected caching via tox. @@ -692,48 +692,48 @@ class Pytester: def path(self) -> Path: """Temporary directory where files are created and pytest is executed.""" return self._path - + def __repr__(self) -> str: return f"<Pytester {self.path!r}>" - + def _finalize(self) -> None: """ Clean up global state artifacts. - - 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 - it can be looked at after the test run has finished. - """ - self._sys_modules_snapshot.restore() - self._sys_path_snapshot.restore() - self._cwd_snapshot.restore() + it can be looked at after the test run has finished. + """ + self._sys_modules_snapshot.restore() + self._sys_path_snapshot.restore() + self._cwd_snapshot.restore() self._monkeypatch.undo() - + def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: # 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. # # Preserve readline due to https://bugs.python.org/issue41033. # pexpect issues a SIGWINCH. - def preserve_module(name): + def preserve_module(name): return name.startswith(("zope", "readline")) - - return SysModulesSnapshot(preserve=preserve_module) - + + return SysModulesSnapshot(preserve=preserve_module) + def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: - """Create a new :py:class:`HookRecorder` for a PluginManager.""" - pluginmanager.reprec = reprec = HookRecorder(pluginmanager) + """Create a new :py:class:`HookRecorder` for a PluginManager.""" + pluginmanager.reprec = reprec = HookRecorder(pluginmanager) self._request.addfinalizer(reprec.finish_recording) - return reprec - + return reprec + def chdir(self) -> None: - """Cd into the temporary directory. - - This is done automatically upon instantiation. - """ + """Cd into the temporary directory. + + This is done automatically upon instantiation. + """ os.chdir(self.path) - + def _makefile( self, ext: str, @@ -742,30 +742,30 @@ class Pytester: encoding: str = "utf-8", ) -> Path: items = list(files.items()) - + def to_text(s: Union[Any, bytes]) -> str: return s.decode(encoding) if isinstance(s, bytes) else str(s) - + if lines: source = "\n".join(to_text(x) for x in lines) basename = self._name - items.insert(0, (basename, source)) - - ret = None - for basename, value in items: + items.insert(0, (basename, source)) + + ret = None + for basename, value in items: p = self.path.joinpath(basename).with_suffix(ext) p.parent.mkdir(parents=True, exist_ok=True) source_ = Source(value) source = "\n".join(to_text(line) for line in source_.lines) p.write_text(source.strip(), encoding=encoding) - if ret is None: - ret = p + if ret is None: + ret = p assert ret is not None - return ret - + return ret + def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: r"""Create new file(s) in the test directory. - + :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. :param args: @@ -775,31 +775,31 @@ class Pytester: :param kwargs: Each keyword is the name of a file, while the value of it will be written as contents of the file. - - Examples: - - .. code-block:: python - + + Examples: + + .. code-block:: python + pytester.makefile(".txt", "line1", "line2") - + pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") - - """ - return self._makefile(ext, args, kwargs) - + + """ + return self._makefile(ext, args, kwargs) + def makeconftest(self, source: str) -> Path: - """Write a contest.py file with 'source' as contents.""" - return self.makepyfile(conftest=source) - + """Write a contest.py file with 'source' as contents.""" + return self.makepyfile(conftest=source) + def makeini(self, source: str) -> Path: - """Write a tox.ini file with 'source' as contents.""" - return self.makefile(".ini", tox=source) - + """Write a tox.ini file with 'source' as contents.""" + return self.makefile(".ini", tox=source) + def getinicfg(self, source: str) -> SectionWrapper: - """Return the pytest section from the tox.ini config file.""" - p = self.makeini(source) + """Return the pytest section from the tox.ini config file.""" + p = self.makeini(source) return IniConfig(str(p))["pytest"] - + def makepyprojecttoml(self, source: str) -> Path: """Write a pyproject.toml file with 'source' as contents. @@ -825,8 +825,8 @@ class Pytester: # 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) -> Path: r"""Shortcut for .makefile() with a .txt extension. @@ -845,38 +845,38 @@ class Pytester: # 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) + def syspathinsert( self, path: Optional[Union[str, "os.PathLike[str]"]] = None ) -> None: - """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. - - This is undone automatically when this object dies at the end of each - test. - """ - if path is None: + """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. + + This is undone automatically when this object dies at the end of each + test. + """ + if path is None: path = self.path - + self._monkeypatch.syspath_prepend(str(path)) - + def mkdir(self, name: str) -> Path: - """Create a new (sub)directory.""" + """Create a new (sub)directory.""" p = self.path / name p.mkdir() return p - + def mkpydir(self, name: str) -> Path: - """Create a new python package. - - This creates a (sub)directory with an empty ``__init__.py`` file so it + """Create a new python package. + + This creates a (sub)directory with an empty ``__init__.py`` file so it gets recognised as a Python package. - """ + """ p = self.path / name p.mkdir() p.joinpath("__init__.py").touch() - return p - + return p + def copy_example(self, name: Optional[str] = None) -> Path: """Copy file from project's directory into the testdir. @@ -885,30 +885,30 @@ class Pytester: """ example_dir = self._request.config.getini("pytester_example_dir") - if example_dir is None: - raise ValueError("pytester_example_dir is unset, can't copy examples") + if example_dir is None: + raise ValueError("pytester_example_dir is unset, can't copy examples") example_dir = Path(str(self._request.config.rootdir)) / example_dir - + for extra_element in self._request.node.iter_markers("pytester_example_path"): - assert extra_element.args + assert extra_element.args example_dir = example_dir.joinpath(*extra_element.args) - - if name is None: + + if name is None: func_name = self._name - maybe_dir = example_dir / func_name - maybe_file = example_dir / (func_name + ".py") - + maybe_dir = example_dir / func_name + maybe_file = example_dir / (func_name + ".py") + if maybe_dir.is_dir(): - example_path = maybe_dir + example_path = maybe_dir elif maybe_file.is_file(): - example_path = maybe_file - else: - raise LookupError( + example_path = maybe_file + else: + raise LookupError( f"{func_name} can't be found as module or package in {example_dir}" - ) - else: + ) + else: example_path = example_dir.joinpath(name) - + if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): # TODO: py.path.local.copy can copy files to existing directories, # while with shutil.copytree the destination directory cannot exist, @@ -918,119 +918,119 @@ class Pytester: elif example_path.is_file(): result = self.path.joinpath(example_path.name) shutil.copy(example_path, result) - return result - else: - raise LookupError( + return result + else: + raise LookupError( f'example "{example_path}" is not found as a file or directory' - ) - - Session = Session - + ) + + Session = Session + def getnode( self, config: Config, arg: Union[str, "os.PathLike[str]"] ) -> Optional[Union[Collector, Item]]: - """Return the collection node of a file. - + """Return the collection node of a file. + :param _pytest.config.Config config: A pytest config. See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it. :param py.path.local arg: Path to the file. - """ + """ session = Session.from_config(config) - assert "::" not in str(arg) - p = py.path.local(arg) - config.hook.pytest_sessionstart(session=session) - res = session.perform_collect([str(p)], genitems=False)[0] + assert "::" not in str(arg) + p = py.path.local(arg) + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([str(p)], genitems=False)[0] config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) - return res - + return res + def getpathnode(self, path: Union[str, "os.PathLike[str]"]): - """Return the collection node of a file. - - This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to - create the (configured) pytest Config instance. - + """Return the collection node of a file. + + This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to + create the (configured) pytest Config instance. + :param py.path.local path: Path to the file. - """ + """ path = py.path.local(path) - config = self.parseconfigure(path) + config = self.parseconfigure(path) session = Session.from_config(config) - x = session.fspath.bestrelpath(path) - config.hook.pytest_sessionstart(session=session) - res = session.perform_collect([x], genitems=False)[0] + x = session.fspath.bestrelpath(path) + config.hook.pytest_sessionstart(session=session) + res = session.perform_collect([x], genitems=False)[0] config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) - return res - + return res + def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: - """Generate all test items from a collection node. - - This recurses into the collection node and returns a list of all the - test items contained within. - """ - session = colitems[0].session + """Generate all test items from a collection node. + + This recurses into the collection node and returns a list of all the + test items contained within. + """ + session = colitems[0].session result: List[Item] = [] - for colitem in colitems: - result.extend(session.genitems(colitem)) - return result - + for colitem in colitems: + result.extend(session.genitems(colitem)) + return result + def runitem(self, source: str) -> Any: - """Run the "test_func" Item. - - The calling test instance (class containing the test method) must - provide a ``.getrunner()`` method which should return a runner which - can run the test protocol for a single item, e.g. - :py:func:`_pytest.runner.runtestprotocol`. - """ - # used from runner functional tests - item = self.getitem(source) - # the test class where we are called from wants to provide the runner + """Run the "test_func" Item. + + The calling test instance (class containing the test method) must + provide a ``.getrunner()`` method which should return a runner which + can run the test protocol for a single item, e.g. + :py:func:`_pytest.runner.runtestprotocol`. + """ + # used from runner functional tests + item = self.getitem(source) + # the test class where we are called from wants to provide the runner testclassinstance = self._request.instance - runner = testclassinstance.getrunner() - return runner(item) - + runner = testclassinstance.getrunner() + return runner(item) + def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder: - """Run a test module in process using ``pytest.main()``. - - This run writes "source" into a temporary file and runs - ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance - for the result. - + """Run a test module in process using ``pytest.main()``. + + This run writes "source" into a temporary file and runs + ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance + for the result. + :param source: The source code of the test module. - + :param cmdlineargs: Any extra command line arguments to use. - + :returns: :py:class:`HookRecorder` instance of the result. - """ - p = self.makepyfile(source) - values = list(cmdlineargs) + [p] - return self.inline_run(*values) - + """ + p = self.makepyfile(source) + values = list(cmdlineargs) + [p] + return self.inline_run(*values) + def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: - """Run ``pytest.main(['--collectonly'])`` in-process. - - 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 - tuple of the collected items and a :py:class:`HookRecorder` instance. - """ - rec = self.inline_run("--collect-only", *args) - items = [x.item for x in rec.getcalls("pytest_itemcollected")] - return items, rec - + """Run ``pytest.main(['--collectonly'])`` in-process. + + 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 + tuple of the collected items and a :py:class:`HookRecorder` instance. + """ + rec = self.inline_run("--collect-only", *args) + items = [x.item for x in rec.getcalls("pytest_itemcollected")] + return items, rec + def inline_run( self, *args: Union[str, "os.PathLike[str]"], plugins=(), no_reraise_ctrlc: bool = False, ) -> HookRecorder: - """Run ``pytest.main()`` in-process, returning a HookRecorder. - - Runs the :py:func:`pytest.main` function to run all of pytest inside - the test process itself. This means it can return a - :py:class:`HookRecorder` instance which gives more detailed results - from that run than can be done by matching stdout/stderr from - :py:meth:`runpytest`. - + """Run ``pytest.main()`` in-process, returning a HookRecorder. + + Runs the :py:func:`pytest.main` function to run all of pytest inside + the test process itself. This means it can return a + :py:class:`HookRecorder` instance which gives more detailed results + from that run than can be done by matching stdout/stderr from + :py:meth:`runpytest`. + :param args: Command line arguments to pass to :py:func:`pytest.main`. :param plugins: @@ -1045,99 +1045,99 @@ class Pytester: # properly between file creation and inline_run (especially if imports # are interspersed with file creation) importlib.invalidate_caches() - + plugins = list(plugins) - finalizers = [] - try: - # Any sys.module or sys.path changes done while running pytest - # inline should be reverted after the test run completes to avoid - # clashing with later inline tests run within the same pytest test, - # e.g. just because they use matching test module names. - finalizers.append(self.__take_sys_modules_snapshot().restore) - finalizers.append(SysPathsSnapshot().restore) - - # Important note: - # - our tests should not leave any other references/registrations - # laying around other than possibly loaded test modules - # referenced from sys.modules, as nothing will clean those up - # automatically - - rec = [] - + finalizers = [] + try: + # Any sys.module or sys.path changes done while running pytest + # inline should be reverted after the test run completes to avoid + # clashing with later inline tests run within the same pytest test, + # e.g. just because they use matching test module names. + finalizers.append(self.__take_sys_modules_snapshot().restore) + finalizers.append(SysPathsSnapshot().restore) + + # Important note: + # - our tests should not leave any other references/registrations + # laying around other than possibly loaded test modules + # referenced from sys.modules, as nothing will clean those up + # automatically + + rec = [] + class Collect: def pytest_configure(x, config: Config) -> None: - rec.append(self.make_hook_recorder(config.pluginmanager)) - - plugins.append(Collect()) + rec.append(self.make_hook_recorder(config.pluginmanager)) + + plugins.append(Collect()) ret = main([str(x) for x in args], plugins=plugins) - if len(rec) == 1: - reprec = rec.pop() - else: - + if len(rec) == 1: + reprec = rec.pop() + else: + class reprec: # type: ignore - pass - + pass + reprec.ret = ret # type: ignore - + # Typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing. if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc: - calls = reprec.getcalls("pytest_keyboard_interrupt") - if calls and calls[-1].excinfo.type == KeyboardInterrupt: - raise KeyboardInterrupt() - return reprec - finally: - for finalizer in finalizers: - finalizer() - + calls = reprec.getcalls("pytest_keyboard_interrupt") + if calls and calls[-1].excinfo.type == KeyboardInterrupt: + raise KeyboardInterrupt() + return reprec + finally: + for finalizer in finalizers: + finalizer() + def runpytest_inprocess( self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any ) -> 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.""" syspathinsert = kwargs.pop("syspathinsert", False) - + if syspathinsert: - self.syspathinsert() + self.syspathinsert() now = timing.time() capture = _get_multicapture("sys") - capture.start_capturing() - try: - try: - reprec = self.inline_run(*args, **kwargs) - except SystemExit as e: + capture.start_capturing() + try: + try: + reprec = self.inline_run(*args, **kwargs) + except SystemExit as e: ret = e.args[0] try: ret = ExitCode(e.args[0]) except ValueError: pass - + class reprec: # type: ignore ret = ret - - except Exception: - traceback.print_exc() - + + except Exception: + traceback.print_exc() + class reprec: # type: ignore ret = ExitCode(3) - - finally: - out, err = capture.readouterr() - capture.stop_capturing() - sys.stdout.write(out) - sys.stderr.write(err) - + + finally: + out, err = capture.readouterr() + capture.stop_capturing() + sys.stdout.write(out) + sys.stderr.write(err) + assert reprec.ret is not None res = RunResult( reprec.ret, out.splitlines(), err.splitlines(), timing.time() - now ) res.reprec = reprec # type: ignore - return res - + return res + def runpytest( self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any ) -> 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`.""" new_args = self._ensure_basetemp(args) if self._method == "inprocess": @@ -1145,128 +1145,128 @@ class Pytester: elif self._method == "subprocess": return self.runpytest_subprocess(*new_args, **kwargs) raise RuntimeError(f"Unrecognized runpytest option: {self._method}") - + def _ensure_basetemp( self, args: Sequence[Union[str, "os.PathLike[str]"]] ) -> List[Union[str, "os.PathLike[str]"]]: new_args = list(args) for x in new_args: if str(x).startswith("--basetemp"): - break - else: + break + else: new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp")) return new_args - + def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: - """Return a new pytest Config instance from given commandline args. - - This invokes the pytest bootstrapping code in _pytest.config to create - a new :py:class:`_pytest.core.PluginManager` and call the - pytest_cmdline_parse hook to create a new - :py:class:`_pytest.config.Config` instance. - - If :py:attr:`plugins` has been populated they should be plugin modules - to be registered with the PluginManager. - """ - import _pytest.config - + """Return a new pytest Config instance from given commandline args. + + This invokes the pytest bootstrapping code in _pytest.config to create + a new :py:class:`_pytest.core.PluginManager` and call the + pytest_cmdline_parse hook to create a new + :py:class:`_pytest.config.Config` instance. + + If :py:attr:`plugins` has been populated they should be plugin modules + to be registered with the PluginManager. + """ + import _pytest.config + new_args = self._ensure_basetemp(args) new_args = [str(x) for x in new_args] config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type] - # we don't know what the test will do with this half-setup config - # object and thus we make sure it gets unconfigured properly in any - # case (otherwise capturing could still be active, for example) + # we don't know what the test will do with this half-setup config + # object and thus we make sure it gets unconfigured properly in any + # case (otherwise capturing could still be active, for example) self._request.addfinalizer(config._ensure_unconfigure) - return config - + return config + def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config: - """Return a new pytest configured Config instance. - + """Return a new pytest configured Config instance. + Returns a new :py:class:`_pytest.config.Config` instance like - :py:meth:`parseconfig`, but also calls the pytest_configure hook. - """ - config = self.parseconfig(*args) - config._do_configure() - return config - + :py:meth:`parseconfig`, but also calls the pytest_configure hook. + """ + config = self.parseconfig(*args) + config._do_configure() + return config + def getitem(self, source: str, funcname: str = "test_func") -> Item: - """Return the test item for a test function. - + """Return the test item for a test function. + Writes the source to a python file and runs pytest's collection on - the resulting module, returning the test item for the requested - function name. - + the resulting module, returning the test item for the requested + function name. + :param source: The module source. :param funcname: The name of the test function for which to return a test item. - """ - items = self.getitems(source) - for item in items: - if item.name == funcname: - return item + """ + items = self.getitems(source) + for item in items: + if item.name == funcname: + return item assert 0, "{!r} item not found in module:\n{}\nitems: {}".format( funcname, source, items - ) - + ) + def getitems(self, source: str) -> List[Item]: - """Return all test items collected from the module. - + """Return all test items collected from the module. + Writes the source to a Python file and runs pytest's collection on - the resulting module, returning all test items contained within. - """ - modcol = self.getmodulecol(source) - return self.genitems([modcol]) - + the resulting module, returning all test items contained within. + """ + modcol = self.getmodulecol(source) + return self.genitems([modcol]) + def getmodulecol( self, source: Union[str, Path], configargs=(), *, withinit: bool = False ): - """Return the module collection node for ``source``. - + """Return the module collection node for ``source``. + Writes ``source`` to a file using :py:meth:`makepyfile` and then - runs the pytest collection on it, returning the collection node for the - test module. - + runs the pytest collection on it, returning the collection node for the + test module. + :param source: The source code of the module to collect. - + :param configargs: 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. - """ - if isinstance(source, Path): + """ + if isinstance(source, Path): path = self.path.joinpath(source) - assert not withinit, "not supported for paths" - else: + assert not withinit, "not supported for paths" + else: kw = {self._name: str(source)} - path = self.makepyfile(**kw) - if withinit: - self.makepyfile(__init__="#") - self.config = config = self.parseconfigure(path, *configargs) - return self.getnode(config, path) - + path = self.makepyfile(**kw) + if withinit: + self.makepyfile(__init__="#") + self.config = config = self.parseconfigure(path, *configargs) + return self.getnode(config, path) + def collect_by_name( self, modcol: Collector, name: str ) -> Optional[Union[Item, Collector]]: - """Return the collection node for name from the module collection. - + """Return the collection node for name from the module collection. + Searchs a module collection node for a collection node matching the given name. - + :param modcol: A module collection node; see :py:meth:`getmodulecol`. :param name: The name of the node to return. - """ - if modcol not in self._mod_collections: - self._mod_collections[modcol] = list(modcol.collect()) - for colitem in self._mod_collections[modcol]: - if colitem.name == name: - return colitem + """ + if modcol not in self._mod_collections: + self._mod_collections[modcol] = list(modcol.collect()) + for colitem in self._mod_collections[modcol]: + if colitem.name == name: + return colitem return None - + def popen( self, cmdargs, @@ -1275,26 +1275,26 @@ class Pytester: stdin=CLOSE_STDIN, **kw, ): - """Invoke subprocess.Popen. - + """Invoke subprocess.Popen. + Calls subprocess.Popen making sure the current working directory is in the PYTHONPATH. - - You probably want to use :py:meth:`run` instead. - """ - env = os.environ.copy() - env["PYTHONPATH"] = os.pathsep.join( - filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) - ) - kw["env"] = env - + + You probably want to use :py:meth:`run` instead. + """ + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join( + filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) + ) + kw["env"] = env + if stdin is self.CLOSE_STDIN: kw["stdin"] = subprocess.PIPE elif isinstance(stdin, bytes): kw["stdin"] = subprocess.PIPE else: kw["stdin"] = stdin - + popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) if stdin is self.CLOSE_STDIN: assert popen.stdin is not None @@ -1303,18 +1303,18 @@ class Pytester: assert popen.stdin is not None popen.stdin.write(stdin) - return popen - + return popen + def run( self, *cmdargs: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None, stdin=CLOSE_STDIN, ) -> RunResult: - """Run a command with arguments. - - Run a process using subprocess.Popen saving the stdout and stderr. - + """Run a command with arguments. + + Run a process using subprocess.Popen saving the stdout and stderr. + :param cmdargs: The sequence of arguments to pass to `subprocess.Popen()`, with path-like objects being converted to ``str`` automatically. @@ -1326,11 +1326,11 @@ class Pytester: the pipe, otherwise it is passed through to ``popen``. Defaults to ``CLOSE_STDIN``, which translates to using a pipe (``subprocess.PIPE``) that gets closed. - + :rtype: RunResult - """ - __tracebackhide__ = True - + """ + __tracebackhide__ = True + # TODO: Remove type ignore in next mypy release. # https://github.com/python/typeshed/pull/4582 cmdargs = tuple( @@ -1338,125 +1338,125 @@ class Pytester: ) p1 = self.path.joinpath("stdout") p2 = self.path.joinpath("stderr") - print("running:", *cmdargs) + print("running:", *cmdargs) print(" in:", Path.cwd()) with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2: now = timing.time() - popen = self.popen( + popen = self.popen( cmdargs, stdin=stdin, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32"), - ) + ) if popen.stdin is not None: popen.stdin.close() - + def handle_timeout() -> None: - __tracebackhide__ = True - - timeout_message = ( - "{seconds} second timeout expired running:" - " {command}".format(seconds=timeout, command=cmdargs) - ) - - popen.kill() - popen.wait() - raise self.TimeoutExpired(timeout_message) - - if timeout is None: - ret = popen.wait() + __tracebackhide__ = True + + timeout_message = ( + "{seconds} second timeout expired running:" + " {command}".format(seconds=timeout, command=cmdargs) + ) + + popen.kill() + popen.wait() + raise self.TimeoutExpired(timeout_message) + + if timeout is None: + ret = popen.wait() else: - try: - ret = popen.wait(timeout) - except subprocess.TimeoutExpired: - handle_timeout() + try: + ret = popen.wait(timeout) + except subprocess.TimeoutExpired: + handle_timeout() with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2: - out = f1.read().splitlines() - err = f2.read().splitlines() + out = f1.read().splitlines() + err = f2.read().splitlines() - self._dump_lines(out, sys.stdout) - self._dump_lines(err, sys.stderr) + self._dump_lines(out, sys.stdout) + self._dump_lines(err, sys.stderr) with contextlib.suppress(ValueError): ret = ExitCode(ret) return RunResult(ret, out, err, timing.time() - now) - - def _dump_lines(self, lines, fp): - try: - for line in lines: - print(line, file=fp) - except UnicodeEncodeError: + + def _dump_lines(self, lines, fp): + try: + for line in lines: + print(line, file=fp) + except UnicodeEncodeError: print(f"couldn't print to {fp} because of encoding") - + def _getpytestargs(self) -> Tuple[str, ...]: - return sys.executable, "-mpytest" - + return sys.executable, "-mpytest" + def runpython(self, script) -> RunResult: - """Run a python script using sys.executable as interpreter. - + """Run a python script using sys.executable as interpreter. + :rtype: RunResult - """ - return self.run(sys.executable, script) - - def runpython_c(self, command): + """ + return self.run(sys.executable, script) + + def runpython_c(self, command): """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: - """Run pytest as a subprocess with given arguments. - - Any plugins added to the :py:attr:`plugins` list will be added using the - ``-p`` command line option. Additionally ``--basetemp`` is used to put - any temporary files and directories in a numbered directory prefixed - with "runpytest-" to not conflict with the normal numbered pytest - location for temporary files and directories. - + """Run pytest as a subprocess with given arguments. + + Any plugins added to the :py:attr:`plugins` list will be added using the + ``-p`` command line option. Additionally ``--basetemp`` is used to put + any temporary files and directories in a numbered directory prefixed + with "runpytest-" to not conflict with the normal numbered pytest + location for temporary files and directories. + :param args: The sequence of arguments to pass to the pytest subprocess. :param timeout: The period in seconds after which to timeout and raise :py:class:`Pytester.TimeoutExpired`. - + :rtype: RunResult - """ - __tracebackhide__ = True + """ + __tracebackhide__ = True p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) - args = ("--basetemp=%s" % p,) + args - plugins = [x for x in self.plugins if isinstance(x, str)] - if plugins: - args = ("-p", plugins[0]) + args - args = self._getpytestargs() + args + args = ("--basetemp=%s" % p,) + args + plugins = [x for x in self.plugins if isinstance(x, str)] + if plugins: + args = ("-p", plugins[0]) + args + args = self._getpytestargs() + args return self.run(*args, timeout=timeout) - + def spawn_pytest( self, string: str, expect_timeout: float = 10.0 ) -> "pexpect.spawn": - """Run pytest using pexpect. - - This makes sure to use the right pytest and sets up the temporary - directory locations. - - The pexpect child is returned. - """ + """Run pytest using pexpect. + + This makes sure to use the right pytest and sets up the temporary + directory locations. + + The pexpect child is returned. + """ basetemp = self.path / "temp-pexpect" basetemp.mkdir(mode=0o700) - invoke = " ".join(map(str, self._getpytestargs())) + invoke = " ".join(map(str, self._getpytestargs())) cmd = f"{invoke} --basetemp={basetemp} {string}" - return self.spawn(cmd, expect_timeout=expect_timeout) - + return self.spawn(cmd, expect_timeout=expect_timeout) + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": - """Run a command using pexpect. - - The pexpect child is returned. - """ + """Run a command using pexpect. + + The pexpect child is returned. + """ pexpect = 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(): skip("pypy-64 bit not supported") if not hasattr(pexpect, "spawn"): skip("pexpect.spawn not available") @@ -1464,27 +1464,27 @@ class Pytester: child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout) self._request.addfinalizer(logfile.close) - return child - - + return child + + class LineComp: def __init__(self) -> None: self.stringio = StringIO() """:class:`python:io.StringIO()` instance used for input.""" - + def assert_contains_lines(self, lines2: Sequence[str]) -> None: """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value. - + Lines are matched using :func:`LineMatcher.fnmatch_lines`. - """ - __tracebackhide__ = True - val = self.stringio.getvalue() - self.stringio.truncate(0) - self.stringio.seek(0) - lines1 = val.split("\n") + """ + __tracebackhide__ = True + val = self.stringio.getvalue() + self.stringio.truncate(0) + self.stringio.seek(0) + lines1 = val.split("\n") LineMatcher(lines1).fnmatch_lines(lines2) - - + + @final @attr.s(repr=False, str=False, init=False) class Testdir: @@ -1697,19 +1697,19 @@ class Testdir: class LineMatcher: - """Flexible matching of text. - - This is a convenience class to test large texts like the output of - commands. - - The constructor takes a list of lines without their trailing newlines, i.e. - ``text.splitlines()``. - """ - + """Flexible matching of text. + + This is a convenience class to test large texts like the output of + commands. + + The constructor takes a list of lines without their trailing newlines, i.e. + ``text.splitlines()``. + """ + def __init__(self, lines: List[str]) -> None: - self.lines = lines + self.lines = lines self._log_output: List[str] = [] - + def __str__(self) -> str: """Return the entire original text. @@ -1719,90 +1719,90 @@ class LineMatcher: return "\n".join(self.lines) def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]: - if isinstance(lines2, str): - lines2 = Source(lines2) - if isinstance(lines2, Source): - lines2 = lines2.strip().lines - return lines2 - + if isinstance(lines2, str): + lines2 = Source(lines2) + if isinstance(lines2, Source): + lines2 = lines2.strip().lines + return lines2 + def fnmatch_lines_random(self, lines2: Sequence[str]) -> None: """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`).""" __tracebackhide__ = True - self._match_lines_random(lines2, fnmatch) - + self._match_lines_random(lines2, fnmatch) + def re_match_lines_random(self, lines2: Sequence[str]) -> None: """Check lines exist in the output in any order (using :func:`python:re.match`).""" __tracebackhide__ = True self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name))) - + def _match_lines_random( self, lines2: Sequence[str], match_func: Callable[[str, str], bool] ) -> None: __tracebackhide__ = True - lines2 = self._getlines(lines2) - for line in lines2: - for x in self.lines: - if line == x or match_func(x, line): - self._log("matched: ", repr(line)) - break - else: + lines2 = self._getlines(lines2) + for line in lines2: + for x in self.lines: + if line == x or match_func(x, line): + self._log("matched: ", repr(line)) + break + else: msg = "line %r not found in output" % line self._log(msg) self._fail(msg) - + def get_lines_after(self, fnline: str) -> Sequence[str]: - """Return all lines following the given line in the text. - - The given line can contain glob wildcards. - """ - for i, line in enumerate(self.lines): - if fnline == line or fnmatch(line, fnline): - return self.lines[i + 1 :] - raise ValueError("line %r not found in output" % fnline) - + """Return all lines following the given line in the text. + + The given line can contain glob wildcards. + """ + for i, line in enumerate(self.lines): + if fnline == line or fnmatch(line, fnline): + return self.lines[i + 1 :] + raise ValueError("line %r not found in output" % fnline) + def _log(self, *args) -> None: self._log_output.append(" ".join(str(x) for x in args)) - - @property + + @property def _log_text(self) -> str: - return "\n".join(self._log_output) - + return "\n".join(self._log_output) + def fnmatch_lines( self, lines2: Sequence[str], *, consecutive: bool = False ) -> None: """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`). - - The argument is a list of lines which have to match and can use glob - wildcards. If they do not match a pytest.fail() is called. The + + The argument is a list of lines which have to match and can use glob + 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. - + :param lines2: String patterns to match. :param consecutive: Match lines consecutively? - """ - __tracebackhide__ = True + """ + __tracebackhide__ = True self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive) - + def re_match_lines( self, lines2: Sequence[str], *, consecutive: bool = False ) -> None: """Check lines exist in the output (using :func:`python:re.match`). - - The argument is a list of lines which have to match using ``re.match``. - If they do not match a pytest.fail() is called. - + + The argument is a list of lines which have to match using ``re.match``. + If they do not match a pytest.fail() is called. + The matches and non-matches are also shown as part of the error message. - + :param lines2: string patterns to match. :param consecutive: match lines consecutively? - """ - __tracebackhide__ = True + """ + __tracebackhide__ = True self._match_lines( lines2, lambda name, pat: bool(re.match(pat, name)), "re.match", consecutive=consecutive, ) - + def _match_lines( self, lines2: Sequence[str], @@ -1811,8 +1811,8 @@ class LineMatcher: *, consecutive: bool = False, ) -> None: - """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. - + """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. + :param Sequence[str] lines2: List of string patterns to match. The actual format depends on ``match_func``. @@ -1825,31 +1825,31 @@ class LineMatcher: when a match occurs. :param consecutive: Match lines consecutively? - """ + """ if not isinstance(lines2, collections.abc.Sequence): raise TypeError("invalid type for lines2: {}".format(type(lines2).__name__)) - lines2 = self._getlines(lines2) - lines1 = self.lines[:] - extralines = [] - __tracebackhide__ = True + lines2 = self._getlines(lines2) + lines1 = self.lines[:] + extralines = [] + __tracebackhide__ = True wnick = len(match_nickname) + 1 started = False - for line in lines2: - nomatchprinted = False - while lines1: - nextline = lines1.pop(0) - if line == nextline: - self._log("exact match:", repr(line)) + for line in lines2: + nomatchprinted = False + while lines1: + nextline = lines1.pop(0) + if line == nextline: + self._log("exact match:", repr(line)) started = True - break - elif match_func(nextline, line): - self._log("%s:" % match_nickname, repr(line)) + break + elif match_func(nextline, line): + self._log("%s:" % match_nickname, repr(line)) self._log( "{:>{width}}".format("with:", width=wnick), repr(nextline) ) started = True - break - else: + break + else: if consecutive and started: msg = f"no consecutive match: {line!r}" self._log(msg) @@ -1857,14 +1857,14 @@ class LineMatcher: "{:>{width}}".format("with:", width=wnick), repr(nextline) ) self._fail(msg) - if not nomatchprinted: + if not nomatchprinted: self._log( "{:>{width}}".format("nomatch:", width=wnick), repr(line) ) - nomatchprinted = True + nomatchprinted = True self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) - extralines.append(nextline) - else: + extralines.append(nextline) + else: msg = f"remains unmatched: {line!r}" self._log(msg) self._fail(msg) diff --git a/contrib/python/pytest/py3/_pytest/python.py b/contrib/python/pytest/py3/_pytest/python.py index c5d6702e0a..f1a47d7d33 100644 --- a/contrib/python/pytest/py3/_pytest/python.py +++ b/contrib/python/pytest/py3/_pytest/python.py @@ -1,12 +1,12 @@ """Python test discovery, setup and run of test functions.""" import enum -import fnmatch -import inspect +import fnmatch +import inspect import itertools -import os -import sys +import os +import sys import types -import warnings +import warnings from collections import Counter from collections import defaultdict from functools import partial @@ -25,95 +25,95 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union - -import py - -import _pytest -from _pytest import fixtures -from _pytest import nodes -from _pytest._code import filter_traceback + +import py + +import _pytest +from _pytest import fixtures +from _pytest import nodes +from _pytest._code import filter_traceback from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr -from _pytest.compat import ascii_escaped +from _pytest.compat import ascii_escaped from _pytest.compat import final -from _pytest.compat import get_default_arg_names -from _pytest.compat import get_real_func -from _pytest.compat import getimfunc -from _pytest.compat import getlocation +from _pytest.compat import get_default_arg_names +from _pytest.compat import get_real_func +from _pytest.compat import getimfunc +from _pytest.compat import getlocation from _pytest.compat import is_async_function -from _pytest.compat import is_generator -from _pytest.compat import NOTSET -from _pytest.compat import REGEX_TYPE -from _pytest.compat import safe_getattr -from _pytest.compat import safe_isclass -from _pytest.compat import STRING_TYPES +from _pytest.compat import is_generator +from _pytest.compat import NOTSET +from _pytest.compat import REGEX_TYPE +from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass +from _pytest.compat import STRING_TYPES from _pytest.config import Config from _pytest.config import ExitCode -from _pytest.config import hookimpl +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session from _pytest.mark import MARK_GEN from _pytest.mark import ParameterSet -from _pytest.mark.structures import get_unpacked_marks +from _pytest.mark.structures import get_unpacked_marks from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator -from _pytest.mark.structures import normalize_mark_list -from _pytest.outcomes import fail +from _pytest.mark.structures import normalize_mark_list +from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.pathlib import import_path from _pytest.pathlib import ImportPathMismatchError -from _pytest.pathlib import parts +from _pytest.pathlib import parts from _pytest.pathlib import visit from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning - + if TYPE_CHECKING: from typing_extensions import Literal from _pytest.fixtures import _Scope - - + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group.addoption( - "--fixtures", - "--funcargs", - action="store_true", - dest="showfixtures", - default=False, - help="show available fixtures, sorted by plugin appearance " - "(fixtures with leading '_' are only shown with '-v')", - ) - group.addoption( - "--fixtures-per-test", - action="store_true", - dest="show_fixtures_per_test", - default=False, - help="show fixtures per test", - ) - parser.addini( - "python_files", - type="args", + group = parser.getgroup("general") + group.addoption( + "--fixtures", + "--funcargs", + action="store_true", + dest="showfixtures", + default=False, + help="show available fixtures, sorted by plugin appearance " + "(fixtures with leading '_' are only shown with '-v')", + ) + group.addoption( + "--fixtures-per-test", + action="store_true", + dest="show_fixtures_per_test", + default=False, + help="show fixtures per test", + ) + parser.addini( + "python_files", + type="args", # NOTE: default is also used in AssertionRewritingHook. - default=["test_*.py", "*_test.py"], - help="glob-style file patterns for Python test module discovery", - ) - parser.addini( - "python_classes", - type="args", - default=["Test"], - help="prefixes or glob names for Python test class discovery", - ) - parser.addini( - "python_functions", - type="args", - default=["test"], - help="prefixes or glob names for Python test function and method discovery", - ) + default=["test_*.py", "*_test.py"], + help="glob-style file patterns for Python test module discovery", + ) + parser.addini( + "python_classes", + type="args", + default=["Test"], + help="prefixes or glob names for Python test class discovery", + ) + parser.addini( + "python_functions", + type="args", + default=["test"], + help="prefixes or glob names for Python test function and method discovery", + ) parser.addini( "disable_test_id_escaping_and_forfeit_all_rights_to_community_support", type="bool", @@ -121,44 +121,44 @@ def pytest_addoption(parser: Parser) -> None: help="disable string escape non-ascii characters, might cause unwanted " "side effects(use at your own risk)", ) - - + + def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: - if config.option.showfixtures: - showfixtures(config) - return 0 - if config.option.show_fixtures_per_test: - show_fixtures_per_test(config) - return 0 + if config.option.showfixtures: + showfixtures(config) + return 0 + if config.option.show_fixtures_per_test: + show_fixtures_per_test(config) + return 0 return None - - + + def pytest_generate_tests(metafunc: "Metafunc") -> None: - for marker in metafunc.definition.iter_markers(name="parametrize"): + for marker in metafunc.definition.iter_markers(name="parametrize"): # TODO: Fix this type-ignore (overlapping kwargs). metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) # type: ignore[misc] - - + + def pytest_configure(config: Config) -> None: - config.addinivalue_line( - "markers", - "parametrize(argnames, argvalues): call a test function multiple " - "times passing in different arguments in turn. argvalues generally " - "needs to be a list of values if argnames specifies only one name " - "or a list of tuples of values if argnames specifies multiple names. " - "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " - "decorated test function, one with arg1=1 and another with arg1=2." + config.addinivalue_line( + "markers", + "parametrize(argnames, argvalues): call a test function multiple " + "times passing in different arguments in turn. argvalues generally " + "needs to be a list of values if argnames specifies only one name " + "or a list of tuples of values if argnames specifies multiple names. " + "Example: @parametrize('arg1', [1,2]) would lead to two calls of the " + "decorated test function, one with arg1=1 and another with arg1=2." "see https://docs.pytest.org/en/stable/parametrize.html for more info " - "and examples.", - ) - config.addinivalue_line( - "markers", - "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " - "all of the specified fixtures. see " + "and examples.", + ) + config.addinivalue_line( + "markers", + "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " + "all of the specified fixtures. see " "https://docs.pytest.org/en/stable/fixture.html#usefixtures ", - ) - - + ) + + def async_warn_and_skip(nodeid: str) -> None: msg = "async def functions are not natively supported and have been skipped.\n" msg += ( @@ -173,9 +173,9 @@ def async_warn_and_skip(nodeid: str) -> None: skip(msg="async def function and no async plugin installed (see warnings)") -@hookimpl(trylast=True) +@hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: - testfunction = pyfuncitem.obj + testfunction = pyfuncitem.obj if is_async_function(testfunction): async_warn_and_skip(pyfuncitem.nodeid) funcargs = pyfuncitem.funcargs @@ -183,76 +183,76 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: result = testfunction(**testargs) if hasattr(result, "__await__") or hasattr(result, "__aiter__"): async_warn_and_skip(pyfuncitem.nodeid) - return True - - + return True + + def pytest_collect_file( path: py.path.local, parent: nodes.Collector ) -> Optional["Module"]: - ext = path.ext - if ext == ".py": - if not parent.session.isinitpath(path): - if not path_matches_patterns( - path, parent.config.getini("python_files") + ["__init__.py"] - ): + ext = path.ext + if ext == ".py": + if not parent.session.isinitpath(path): + if not path_matches_patterns( + path, parent.config.getini("python_files") + ["__init__.py"] + ): return None - ihook = parent.session.gethookproxy(path) + ihook = parent.session.gethookproxy(path) module: Module = ihook.pytest_pycollect_makemodule(path=path, parent=parent) return module return None - - + + def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool: """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) + + def pytest_pycollect_makemodule(path: py.path.local, parent) -> "Module": - if path.basename == "__init__.py": + if path.basename == "__init__.py": pkg: Package = Package.from_parent(parent, fspath=path) return pkg mod: Module = Module.from_parent(parent, fspath=path) return mod - - + + @hookimpl(trylast=True) def pytest_pycollect_makeitem(collector: "PyCollector", name: str, obj: object): # Nothing was collected elsewhere, let's do it here. - if safe_isclass(obj): - if collector.istestclass(obj, name): + if safe_isclass(obj): + if collector.istestclass(obj, name): 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. - obj = getattr(obj, "__func__", obj) - # We need to try and unwrap the function if it's a functools.partial + obj = getattr(obj, "__func__", obj) + # We need to try and unwrap the function if it's a functools.partial # or a functools.wrapped. # 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))): - filename, lineno = getfslineno(obj) - warnings.warn_explicit( + filename, lineno = getfslineno(obj) + warnings.warn_explicit( message=PytestCollectionWarning( - "cannot collect %r because it is not a function." % name - ), - category=None, - filename=str(filename), - lineno=lineno + 1, - ) - elif getattr(obj, "__test__", True): - if is_generator(obj): + "cannot collect %r because it is not a function." % name + ), + category=None, + filename=str(filename), + lineno=lineno + 1, + ) + elif getattr(obj, "__test__", True): + if is_generator(obj): res = Function.from_parent(collector, name=name) reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( name=name ) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) res.warn(PytestCollectionWarning(reason)) - else: - res = list(collector._genfunctions(name, obj)) + else: + res = list(collector._genfunctions(name, obj)) return res - - + + class PyobjMixin: - _ALLOW_MARKERS = True - + _ALLOW_MARKERS = True + # Function and attributes that the mixin needs (for type-checking only). if TYPE_CHECKING: name: str = "" @@ -294,55 +294,55 @@ class PyobjMixin: if self._ALLOW_MARKERS: self.own_markers.extend(get_unpacked_marks(self.obj)) return obj - + @obj.setter def obj(self, value): self._obj = value - - def _getobj(self): + + def _getobj(self): """Get the underlying Python object. May be overwritten by subclasses.""" # TODO: Improve the type of `parent` such that assert/ignore aren't needed. assert self.parent is not None obj = self.parent.obj # type: ignore[attr-defined] return getattr(obj, self.name) - + def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: """Return Python path relative to the containing module.""" - chain = self.listchain() - chain.reverse() - parts = [] - for node in chain: - if isinstance(node, Instance): - continue - name = node.name - if isinstance(node, Module): - name = os.path.splitext(name)[0] - if stopatmodule: - if includemodule: - parts.append(name) - break - parts.append(name) - parts.reverse() + chain = self.listchain() + chain.reverse() + parts = [] + for node in chain: + if isinstance(node, Instance): + continue + name = node.name + if isinstance(node, Module): + name = os.path.splitext(name)[0] + if stopatmodule: + if includemodule: + parts.append(name) + break + parts.append(name) + parts.reverse() return ".".join(parts) - + def reportinfo(self) -> Tuple[Union[py.path.local, str], int, str]: - # XXX caching? - obj = self.obj - compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) - if isinstance(compat_co_firstlineno, int): - # nose compatibility + # XXX caching? + obj = self.obj + compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) + if isinstance(compat_co_firstlineno, int): + # nose compatibility file_path = sys.modules[obj.__module__].__file__ if file_path.endswith(".pyc"): file_path = file_path[:-1] fspath: Union[py.path.local, str] = file_path - lineno = compat_co_firstlineno - else: - fspath, lineno = getfslineno(obj) - modpath = self.getmodpath() - assert isinstance(lineno, int) - return fspath, lineno, modpath - - + lineno = compat_co_firstlineno + else: + fspath, lineno = getfslineno(obj) + modpath = self.getmodpath() + assert isinstance(lineno, int) + return fspath, lineno, modpath + + # As an optimization, these builtin attribute names are pre-ignored when # iterating over an object during collection -- the pytest_pycollect_makeitem # hook is not called for them. @@ -363,78 +363,78 @@ del _EmptyClass # fmt: on -class PyCollector(PyobjMixin, nodes.Collector): +class PyCollector(PyobjMixin, nodes.Collector): def funcnamefilter(self, name: str) -> bool: - 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: """Look for the __test__ attribute, which is applied by the @nose.tools.istest decorator. - """ - # We explicitly check for "is True" here to not mistakenly treat - # classes with a custom __getattr__ returning something truthy (like a - # function) as test classes. - return safe_getattr(obj, "__test__", False) is True - + """ + # We explicitly check for "is True" here to not mistakenly treat + # classes with a custom __getattr__ returning something truthy (like a + # function) as test classes. + return safe_getattr(obj, "__test__", False) is True + def classnamefilter(self, name: str) -> bool: - return self._matches_prefix_or_glob_option("python_classes", name) - + return self._matches_prefix_or_glob_option("python_classes", name) + def istestfunction(self, obj: object, name: str) -> bool: - if self.funcnamefilter(name) or self.isnosetest(obj): - if isinstance(obj, staticmethod): + if self.funcnamefilter(name) or self.isnosetest(obj): + if isinstance(obj, staticmethod): # staticmethods need to be unwrapped. - obj = safe_getattr(obj, "__func__", False) - return ( - safe_getattr(obj, "__call__", False) - and fixtures.getfixturemarker(obj) is None - ) - else: - return False - + obj = safe_getattr(obj, "__func__", False) + return ( + safe_getattr(obj, "__call__", False) + and fixtures.getfixturemarker(obj) is None + ) + else: + return False + def istestclass(self, obj: object, name: str) -> bool: - 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: """Check if the given name matches the prefix or glob-pattern defined in ini configuration.""" - for option in self.config.getini(option_name): - if name.startswith(option): - return True + for option in self.config.getini(option_name): + if name.startswith(option): + return True # 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. - elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch( - name, option - ): - return True - return False - + elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch( + name, option + ): + return True + return False + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - if not getattr(self.obj, "__test__", True): - return [] - - # NB. we avoid random getattrs and peek in the __dict__ instead - # (XXX originally introduced from a PyPy need, still true?) - dicts = [getattr(self.obj, "__dict__", {})] + if not getattr(self.obj, "__test__", True): + return [] + + # NB. we avoid random getattrs and peek in the __dict__ instead + # (XXX originally introduced from a PyPy need, still true?) + dicts = [getattr(self.obj, "__dict__", {})] for basecls in self.obj.__class__.__mro__: - dicts.append(basecls.__dict__) + dicts.append(basecls.__dict__) seen: Set[str] = set() values: List[Union[nodes.Item, nodes.Collector]] = [] ihook = self.ihook - for dic in dicts: + for dic in dicts: # Note: seems like the dict can change during iteration - # be careful not to remove the list() without consideration. - for name, obj in list(dic.items()): + for name, obj in list(dic.items()): if name in IGNORED_ATTRIBUTES: continue - if name in seen: - continue + if name in seen: + continue seen.add(name) res = ihook.pytest_pycollect_makeitem( collector=self, name=name, obj=obj ) - if res is None: - continue + if res is None: + continue elif isinstance(res, list): values.extend(res) else: @@ -445,66 +445,66 @@ class PyCollector(PyobjMixin, nodes.Collector): return (str(fspath), lineno) values.sort(key=sort_key) - return values - + return values + def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: modulecol = self.getparent(Module) assert modulecol is not None module = modulecol.obj - clscol = self.getparent(Class) - cls = clscol and clscol.obj or None - fm = self.session._fixturemanager - + clscol = self.getparent(Class) + cls = clscol and clscol.obj or None + fm = self.session._fixturemanager + definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) fixtureinfo = definition._fixtureinfo - - metafunc = Metafunc( - definition, fixtureinfo, self.config, cls=cls, module=module - ) - methods = [] - if hasattr(module, "pytest_generate_tests"): - methods.append(module.pytest_generate_tests) + + metafunc = Metafunc( + definition, fixtureinfo, self.config, cls=cls, module=module + ) + methods = [] + if hasattr(module, "pytest_generate_tests"): + methods.append(module.pytest_generate_tests) if cls is not None and hasattr(cls, "pytest_generate_tests"): - methods.append(cls().pytest_generate_tests) - + methods.append(cls().pytest_generate_tests) + self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) - if not metafunc._calls: + if not metafunc._calls: yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) - else: + else: # 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 - # with direct parametrization, so make sure we update what the - # function really needs. - fixtureinfo.prune_dependency_tree() - - for callspec in metafunc._calls: + # with direct parametrization, so make sure we update what the + # function really needs. + fixtureinfo.prune_dependency_tree() + + for callspec in metafunc._calls: subname = f"{name}[{callspec.id}]" yield Function.from_parent( self, - name=subname, - callspec=callspec, - callobj=funcobj, - fixtureinfo=fixtureinfo, - keywords={callspec.id: True}, - originalname=name, - ) - - -class Module(nodes.File, PyCollector): + name=subname, + callspec=callspec, + callobj=funcobj, + fixtureinfo=fixtureinfo, + keywords={callspec.id: True}, + originalname=name, + ) + + +class Module(nodes.File, PyCollector): """Collector for test classes and functions.""" - - def _getobj(self): - return self._importtestmodule() - + + def _getobj(self): + return self._importtestmodule() + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: self._inject_setup_module_fixture() self._inject_setup_function_fixture() - self.session._fixturemanager.parsefactories(self) + self.session._fixturemanager.parsefactories(self) return super().collect() - + def _inject_setup_module_fixture(self) -> None: """Inject a hidden autouse, module scoped fixture into the collected module object that invokes setUpModule/tearDownModule if either or both are available. @@ -571,55 +571,55 @@ class Module(nodes.File, PyCollector): self.obj.__pytest_setup_function = xunit_setup_function_fixture - def _importtestmodule(self): + def _importtestmodule(self): # We assume we are only called once per module. - importmode = self.config.getoption("--import-mode") - try: + importmode = self.config.getoption("--import-mode") + try: mod = import_path(self.fspath, mode=importmode) except SyntaxError as e: - raise self.CollectError( + raise self.CollectError( ExceptionInfo.from_current().getrepr(style="short") ) from e except ImportPathMismatchError as e: raise self.CollectError( - "import file mismatch:\n" - "imported module %r has this __file__ attribute:\n" - " %s\n" - "which is not the same as the test file we want to collect:\n" - " %s\n" - "HINT: remove __pycache__ / .pyc files and/or use a " - "unique basename for your test file modules" % e.args + "import file mismatch:\n" + "imported module %r has this __file__ attribute:\n" + " %s\n" + "which is not the same as the test file we want to collect:\n" + " %s\n" + "HINT: remove __pycache__ / .pyc files and/or use a " + "unique basename for your test file modules" % e.args ) from e except ImportError as e: exc_info = ExceptionInfo.from_current() - if self.config.getoption("verbose") < 2: - exc_info.traceback = exc_info.traceback.filter(filter_traceback) - exc_repr = ( - exc_info.getrepr(style="short") - if exc_info.traceback - else exc_info.exconly() - ) + if self.config.getoption("verbose") < 2: + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = ( + exc_info.getrepr(style="short") + if exc_info.traceback + else exc_info.exconly() + ) formatted_tb = str(exc_repr) - raise self.CollectError( - "ImportError while importing test module '{fspath}'.\n" - "Hint: make sure your test modules/packages have valid Python names.\n" - "Traceback:\n" - "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) + raise self.CollectError( + "ImportError while importing test module '{fspath}'.\n" + "Hint: make sure your test modules/packages have valid Python names.\n" + "Traceback:\n" + "{traceback}".format(fspath=self.fspath, traceback=formatted_tb) ) from e except skip.Exception as e: - if e.allow_module_level: - raise - raise self.CollectError( - "Using pytest.skip outside of a test is not allowed. " - "To decorate a test function, use the @pytest.mark.skip " - "or @pytest.mark.skipif decorators instead, and to skip a " - "module use `pytestmark = pytest.mark.{skip,skipif}." + if e.allow_module_level: + raise + raise self.CollectError( + "Using pytest.skip outside of a test is not allowed. " + "To decorate a test function, use the @pytest.mark.skip " + "or @pytest.mark.skipif decorators instead, and to skip a " + "module use `pytestmark = pytest.mark.{skip,skipif}." ) from e - self.config.pluginmanager.consider_module(mod) - return mod - - -class Package(Module): + self.config.pluginmanager.consider_module(mod) + return mod + + +class Package(Module): def __init__( self, fspath: py.path.local, @@ -631,10 +631,10 @@ class Package(Module): ) -> None: # NOTE: Could be just the following, but kept as-is for compat. # nodes.FSCollector.__init__(self, fspath, parent=parent) - session = parent.session - nodes.FSCollector.__init__( - self, fspath, parent=parent, config=config, session=session, nodeid=nodeid - ) + session = parent.session + nodes.FSCollector.__init__( + self, fspath, parent=parent, config=config, session=session, nodeid=nodeid + ) self.name = os.path.basename(str(fspath.dirname)) def setup(self) -> None: @@ -656,11 +656,11 @@ class Package(Module): def gethookproxy(self, fspath: py.path.local): warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.gethookproxy(fspath) - + def isinitpath(self, path: py.path.local) -> bool: warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2) return self.session.isinitpath(path) - + def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": return False @@ -698,37 +698,37 @@ class Package(Module): return ihook.pytest_collect_file(path=path, parent=self) # type: ignore[no-any-return] def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - this_path = self.fspath.dirpath() - init_module = this_path.join("__init__.py") - if init_module.check(file=1) and path_matches_patterns( - init_module, self.config.getini("python_files") - ): + this_path = self.fspath.dirpath() + init_module = this_path.join("__init__.py") + if init_module.check(file=1) and path_matches_patterns( + init_module, self.config.getini("python_files") + ): yield Module.from_parent(self, fspath=init_module) pkg_prefixes: Set[py.path.local] = set() for direntry in visit(str(this_path), recurse=self._recurse): path = py.path.local(direntry.path) - # We will visit our own __init__.py file, in which case we skip it. + # We will visit our own __init__.py file, in which case we skip it. if direntry.is_file(): if direntry.name == "__init__.py" and path.dirpath() == this_path: - continue - + continue + parts_ = parts(direntry.path) - if any( + if any( str(pkg_prefix) in parts_ and pkg_prefix.join("__init__.py") != path - for pkg_prefix in pkg_prefixes - ): - continue - + for pkg_prefix in pkg_prefixes + ): + continue + if direntry.is_file(): yield from self._collectfile(path) elif not direntry.is_dir(): # Broken symlink or invalid/missing file. continue elif path.join("__init__.py").check(file=1): - pkg_prefixes.add(path) - - + pkg_prefixes.add(path) + + def _call_with_optional_argument(func, arg) -> None: """Call the given function with the given argument if func accepts one argument, otherwise calls func without arguments.""" @@ -742,55 +742,55 @@ def _call_with_optional_argument(func, arg) -> None: 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 avoid calling it twice.""" for name in names: meth = getattr(obj, name, None) if meth is not None and fixtures.getfixturemarker(meth) is None: return meth - - -class Class(PyCollector): + + +class Class(PyCollector): """Collector for test methods.""" - + @classmethod def from_parent(cls, parent, *, name, obj=None): """The public constructor.""" return super().from_parent(name=name, parent=parent) def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - if not safe_getattr(self.obj, "__test__", True): - return [] - if hasinit(self.obj): + if not safe_getattr(self.obj, "__test__", True): + return [] + if hasinit(self.obj): assert self.parent is not None - self.warn( + self.warn( PytestCollectionWarning( - "cannot collect test class %r because it has a " + "cannot collect test class %r because it has a " "__init__ constructor (from: %s)" % (self.obj.__name__, self.parent.nodeid) - ) - ) - return [] - elif hasnew(self.obj): + ) + ) + return [] + elif hasnew(self.obj): assert self.parent is not None - self.warn( + self.warn( PytestCollectionWarning( - "cannot collect test class %r because it has a " + "cannot collect test class %r because it has a " "__new__ constructor (from: %s)" % (self.obj.__name__, self.parent.nodeid) - ) - ) - return [] - + ) + ) + return [] + self._inject_setup_class_fixture() self._inject_setup_method_fixture() - + return [Instance.from_parent(self, name="()")] - + def _inject_setup_class_fixture(self) -> None: """Inject a hidden autouse, class scoped fixture into the collected class object 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 other fixtures (#517). """ @@ -847,45 +847,45 @@ class Class(PyCollector): self.obj.__pytest_setup_method = xunit_setup_method_fixture -class Instance(PyCollector): - _ALLOW_MARKERS = False # hack, destroy later +class Instance(PyCollector): + _ALLOW_MARKERS = False # hack, destroy later # 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. - - def _getobj(self): + + def _getobj(self): # TODO: Improve the type of `parent` such that assert/ignore aren't needed. assert self.parent is not None obj = self.parent.obj # type: ignore[attr-defined] return obj() - + def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: - self.session._fixturemanager.parsefactories(self) + self.session._fixturemanager.parsefactories(self) return super().collect() - - def newinstance(self): - self.obj = self._getobj() - return self.obj - - + + def newinstance(self): + self.obj = self._getobj() + return self.obj + + def hasinit(obj: object) -> bool: init: object = getattr(obj, "__init__", None) - if init: - return init != object.__init__ + if init: + return init != object.__init__ return False - - + + def hasnew(obj: object) -> bool: new: object = getattr(obj, "__new__", None) - if new: - return new != object.__new__ + if new: + return new != object.__new__ return False - - + + @final class CallSpec2: def __init__(self, metafunc: "Metafunc") -> None: - self.metafunc = metafunc + self.metafunc = metafunc self.funcargs: Dict[str, object] = {} self._idlist: List[str] = [] self.params: Dict[str, object] = {} @@ -893,31 +893,31 @@ class CallSpec2: self._arg2scopenum: Dict[str, int] = {} self.marks: List[Mark] = [] self.indices: Dict[str, int] = {} - + def copy(self) -> "CallSpec2": - cs = CallSpec2(self.metafunc) - cs.funcargs.update(self.funcargs) - cs.params.update(self.params) - cs.marks.extend(self.marks) - cs.indices.update(self.indices) - cs._arg2scopenum.update(self._arg2scopenum) - cs._idlist = list(self._idlist) - return cs - + cs = CallSpec2(self.metafunc) + cs.funcargs.update(self.funcargs) + cs.params.update(self.params) + cs.marks.extend(self.marks) + cs.indices.update(self.indices) + cs._arg2scopenum.update(self._arg2scopenum) + cs._idlist = list(self._idlist) + return cs + def _checkargnotcontained(self, arg: str) -> None: - if arg in self.params or arg in self.funcargs: + if arg in self.params or arg in self.funcargs: raise ValueError(f"duplicate {arg!r}") - + def getparam(self, name: str) -> object: - try: - return self.params[name] + try: + return self.params[name] except KeyError as e: raise ValueError(name) from e - - @property + + @property def id(self) -> str: return "-".join(map(str, self._idlist)) - + def setmulti2( self, valtypes: Mapping[str, "Literal['params', 'funcargs']"], @@ -928,30 +928,30 @@ class CallSpec2: scopenum: int, param_index: int, ) -> None: - for arg, val in zip(argnames, valset): - self._checkargnotcontained(arg) - valtype_for_arg = valtypes[arg] + for arg, val in zip(argnames, valset): + self._checkargnotcontained(arg) + valtype_for_arg = valtypes[arg] if valtype_for_arg == "params": self.params[arg] = val elif valtype_for_arg == "funcargs": self.funcargs[arg] = val else: # pragma: no cover assert False, f"Unhandled valtype for arg: {valtype_for_arg}" - self.indices[arg] = param_index - self._arg2scopenum[arg] = scopenum - self._idlist.append(id) - self.marks.extend(normalize_mark_list(marks)) - - + self.indices[arg] = param_index + self._arg2scopenum[arg] = scopenum + self._idlist.append(id) + self.marks.extend(normalize_mark_list(marks)) + + @final class Metafunc: """Objects 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 - test configuration or values specified in the class or module where a - test function is defined. - """ - + 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 function is defined. + """ + def __init__( self, definition: "FunctionDefinition", @@ -961,26 +961,26 @@ class Metafunc: module=None, ) -> None: #: Access to the underlying :class:`_pytest.python.FunctionDefinition`. - self.definition = definition - + self.definition = definition + #: 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. - self.module = module - + self.module = module + #: Underlying Python test function. - self.function = definition.obj - + self.function = definition.obj + #: 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``. - self.cls = cls - + self.cls = cls + self._calls: List[CallSpec2] = [] - self._arg2fixturedefs = fixtureinfo.name2fixturedefs - + self._arg2fixturedefs = fixtureinfo.name2fixturedefs + def parametrize( self, argnames: Union[str, List[str], Tuple[str, ...]], @@ -997,18 +997,18 @@ class Metafunc: _param_mark: Optional[Mark] = None, ) -> None: """Add new invocations to the underlying test function using the list - of argvalues for the given argnames. Parametrization is performed - during the collection phase. If you need to setup expensive resources - see about setting indirect to do it rather at test setup time. - + of argvalues for the given argnames. Parametrization is performed + during the collection phase. If you need to setup expensive resources + see about setting indirect to do it rather at test setup time. + :param argnames: A comma-separated string denoting one or more argument names, or a list/tuple of argument strings. - + :param argvalues: The list of argvalues determines how often a test is invoked with different argument values. - + If only one argname was specified argvalues is a list of values. If N argnames were specified, argvalues must be a list of N-tuples, where each tuple-element specifies a value for its @@ -1018,10 +1018,10 @@ class Metafunc: 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 - function so that it can perform more expensive setups during the - setup phase of a test rather than at collection time. - + be passed as request.param to its respective argname fixture + function so that it can perform more expensive setups during the + setup phase of a test rather than at collection time. + :param ids: Sequence of (or generator for) ids for ``argvalues``, or a callable to return part of the id for each argvalue. @@ -1039,39 +1039,39 @@ class Metafunc: This is useful to provide more specific ids for certain items, e.g. dates. Returning ``None`` will use an auto-generated id. - If no ids are provided they will be generated automatically from - the argvalues. - + If no ids are provided they will be generated automatically from + the argvalues. + :param scope: If specified it denotes the scope of the parameters. - The scope is used for grouping tests by parameter instances. - It will also override any fixture-function defined scope, allowing - to set a dynamic scope using test context or configuration. - """ - from _pytest.fixtures import scope2index - - argnames, parameters = ParameterSet._for_parametrize( - argnames, - argvalues, - self.function, - self.config, + The scope is used for grouping tests by parameter instances. + It will also override any fixture-function defined scope, allowing + to set a dynamic scope using test context or configuration. + """ + from _pytest.fixtures import scope2index + + argnames, parameters = ParameterSet._for_parametrize( + argnames, + argvalues, + self.function, + self.config, nodeid=self.definition.nodeid, - ) - del argvalues - + ) + del argvalues + if "request" in argnames: fail( "'request' is a reserved name and cannot be used in @pytest.mark.parametrize", pytrace=False, ) - if scope is None: - scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) - - self._validate_if_using_arg_names(argnames, indirect) - - arg_values_types = self._resolve_arg_value_types(argnames, indirect) - + if scope is None: + scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) + + self._validate_if_using_arg_names(argnames, indirect) + + arg_values_types = self._resolve_arg_value_types(argnames, indirect) + # Use any already (possibly) generated ids with parametrize Marks. if _param_mark and _param_mark._param_ids_from: generated_ids = _param_mark._param_ids_from._param_ids_generated @@ -1081,34 +1081,34 @@ class Metafunc: ids = self._resolve_arg_ids( argnames, ids, parameters, nodeid=self.definition.nodeid ) - + # Store used (possibly generated) ids with parametrize Marks. if _param_mark and _param_mark._param_ids_from and generated_ids is None: object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) - scopenum = scope2index( + scopenum = scope2index( scope, descr=f"parametrize() call in {self.function.__name__}" - ) - + ) + # 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. - newcalls = [] - for callspec in self._calls or [CallSpec2(self)]: - for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)): - newcallspec = callspec.copy() - newcallspec.setmulti2( - arg_values_types, - argnames, - param_set.values, - param_id, - param_set.marks, - scopenum, - param_index, - ) - newcalls.append(newcallspec) - self._calls = newcalls - + newcalls = [] + for callspec in self._calls or [CallSpec2(self)]: + for param_index, (param_id, param_set) in enumerate(zip(ids, parameters)): + newcallspec = callspec.copy() + newcallspec.setmulti2( + arg_values_types, + argnames, + param_set.values, + param_id, + param_set.marks, + scopenum, + param_index, + ) + newcalls.append(newcallspec) + self._calls = newcalls + def _resolve_arg_ids( self, argnames: Sequence[str], @@ -1122,26 +1122,26 @@ class Metafunc: nodeid: str, ) -> List[str]: """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 ids: The ids parameter of the parametrized call (see docs). :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. - :rtype: List[str] + :rtype: List[str] :returns: The list of ids for each argname given. - """ + """ if ids is None: idfn = None ids_ = None elif callable(ids): - idfn = ids + idfn = ids ids_ = None else: idfn = None ids_ = self._validate_ids(ids, parameters, self.function.__name__) return idmaker(argnames, parameters, idfn, ids_, self.config, nodeid=nodeid) - + def _validate_ids( self, ids: Iterable[Union[None, str, float, int, bool]], @@ -1185,29 +1185,29 @@ class Metafunc: """Resolve if each parametrized argument must be considered a 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 indirect: Same as the ``indirect`` parameter of ``parametrize()``. - :rtype: Dict[str, str] - A dict mapping each arg name to either: - * "params" if the argname should be the parameter of a fixture of the same name. - * "funcargs" if the argname should be a parameter to the parametrized test function. - """ + :rtype: Dict[str, str] + A dict mapping each arg name to either: + * "params" if the argname should be the parameter of a fixture of the same name. + * "funcargs" if the argname should be a parameter to the parametrized test function. + """ if isinstance(indirect, bool): valtypes: Dict[str, Literal["params", "funcargs"]] = dict.fromkeys( argnames, "params" if indirect else "funcargs" ) elif isinstance(indirect, Sequence): - valtypes = dict.fromkeys(argnames, "funcargs") - for arg in indirect: - if arg not in argnames: - fail( - "In {}: indirect fixture '{}' doesn't exist".format( - self.function.__name__, arg - ), - pytrace=False, - ) - valtypes[arg] = "params" + valtypes = dict.fromkeys(argnames, "funcargs") + for arg in indirect: + if arg not in argnames: + fail( + "In {}: indirect fixture '{}' doesn't exist".format( + self.function.__name__, arg + ), + pytrace=False, + ) + valtypes[arg] = "params" else: fail( "In {func}: expected Sequence or boolean for indirect, got {type}".format( @@ -1215,74 +1215,74 @@ class Metafunc: ), pytrace=False, ) - return valtypes - + return valtypes + def _validate_if_using_arg_names( self, argnames: Sequence[str], indirect: Union[bool, Sequence[str]], ) -> None: """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 indirect: Same as the ``indirect`` parameter of ``parametrize()``. :raises ValueError: If validation fails. - """ - default_arg_names = set(get_default_arg_names(self.function)) - func_name = self.function.__name__ - for arg in argnames: - if arg not in self.fixturenames: - if arg in default_arg_names: - fail( - "In {}: function already takes an argument '{}' with a default value".format( - func_name, arg - ), - pytrace=False, - ) - else: + """ + default_arg_names = set(get_default_arg_names(self.function)) + func_name = self.function.__name__ + for arg in argnames: + if arg not in self.fixturenames: + if arg in default_arg_names: + fail( + "In {}: function already takes an argument '{}' with a default value".format( + func_name, arg + ), + pytrace=False, + ) + else: if isinstance(indirect, Sequence): - name = "fixture" if arg in indirect else "argument" - else: - name = "fixture" if indirect else "argument" - fail( + name = "fixture" if arg in indirect else "argument" + else: + name = "fixture" if indirect else "argument" + fail( f"In {func_name}: function uses no {name} '{arg}'", - pytrace=False, - ) - - + pytrace=False, + ) + + def _find_parametrized_scope( argnames: Sequence[str], arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], indirect: Union[bool, Sequence[str]], ) -> "fixtures._Scope": - """Find the most appropriate scope for a parametrized call based on its arguments. - - When there's at least one direct argument, always use "function" scope. - - When a test function is parametrized and all its arguments are indirect - (e.g. fixtures), return the most narrow scope based on the fixtures used. - - Related to issue #1832, based on code posted by @Kingdread. - """ + """Find the most appropriate scope for a parametrized call based on its arguments. + + When there's at least one direct argument, always use "function" scope. + + When a test function is parametrized and all its arguments are indirect + (e.g. fixtures), return the most narrow scope based on the fixtures used. + + Related to issue #1832, based on code posted by @Kingdread. + """ if isinstance(indirect, Sequence): - all_arguments_are_fixtures = len(indirect) == len(argnames) - else: - all_arguments_are_fixtures = bool(indirect) - - if all_arguments_are_fixtures: - fixturedefs = arg2fixturedefs or {} - used_scopes = [ - fixturedef[0].scope - for name, fixturedef in fixturedefs.items() - if name in argnames - ] - if used_scopes: + all_arguments_are_fixtures = len(indirect) == len(argnames) + else: + all_arguments_are_fixtures = bool(indirect) + + if all_arguments_are_fixtures: + fixturedefs = arg2fixturedefs or {} + used_scopes = [ + fixturedef[0].scope + for name, fixturedef in fixturedefs.items() + if name in argnames + ] + if used_scopes: # Takes the most narrow scope from used fixtures. for scope in reversed(fixtures.scopes): - if scope in used_scopes: - return scope - - return "function" - - + if scope in used_scopes: + return scope + + return "function" + + def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str: if config is None: escape_option = False @@ -1304,41 +1304,41 @@ def _idval( nodeid: Optional[str], config: Optional[Config], ) -> str: - if idfn: - try: + if idfn: + try: generated_id = idfn(val) if generated_id is not None: val = generated_id - except Exception as e: + except Exception as e: prefix = f"{nodeid}: " if nodeid is not None else "" msg = "error raised while trying to determine id of parameter '{}' at position {}" msg = prefix + msg.format(argname, idx) raise ValueError(msg) from e elif config: hook_id: Optional[str] = config.hook.pytest_make_parametrize_id( - config=config, val=val, argname=argname + config=config, val=val, argname=argname ) - if hook_id: - return hook_id - - if isinstance(val, STRING_TYPES): + if hook_id: + return hook_id + + if isinstance(val, STRING_TYPES): return _ascii_escaped_by_config(val, config) elif val is None or isinstance(val, (float, int, bool)): - return str(val) - elif isinstance(val, REGEX_TYPE): - return ascii_escaped(val.pattern) + return str(val) + elif isinstance(val, REGEX_TYPE): + return ascii_escaped(val.pattern) elif val is NOTSET: # Fallback to default. Note that NOTSET is an enum.Enum. pass elif isinstance(val, enum.Enum): - return str(val) + return str(val) elif isinstance(getattr(val, "__name__", None), str): # Name of a class, function, module, etc. name: str = getattr(val, "__name__") return name - return str(argname) + str(idx) - - + return str(argname) + str(idx) + + def limit_idval(limit): import functools @@ -1375,19 +1375,19 @@ def _idvalset( nodeid: Optional[str], config: Optional[Config], ) -> str: - if parameterset.id is not None: - return parameterset.id + if parameterset.id is not None: + return parameterset.id id = None if ids is None or idx >= len(ids) else ids[idx] if id is None: - this_id = [ + this_id = [ _idval(val, argname, idx, idfn, nodeid=nodeid, config=config) - for val, argname in zip(parameterset.values, argnames) - ] - return "-".join(this_id) - else: + for val, argname in zip(parameterset.values, argnames) + ] + return "-".join(this_id) + else: return _ascii_escaped_by_config(id, config) - - + + def idmaker( argnames: Iterable[str], parametersets: Iterable[ParameterSet], @@ -1400,13 +1400,13 @@ def idmaker( _idvalset( valindex, parameterset, argnames, idfn, ids, config=config, nodeid=nodeid ) - for valindex, parameterset in enumerate(parametersets) - ] - + for valindex, parameterset in enumerate(parametersets) + ] + # All IDs must be unique! unique_ids = set(resolved_ids) if len(unique_ids) != len(resolved_ids): - + # Record the number of occurrences of each test ID. test_id_counts = Counter(resolved_ids) @@ -1422,130 +1422,130 @@ def idmaker( return resolved_ids -def show_fixtures_per_test(config): - from _pytest.main import wrap_session - - return wrap_session(config, _show_fixtures_per_test) - - +def show_fixtures_per_test(config): + from _pytest.main import wrap_session + + return wrap_session(config, _show_fixtures_per_test) + + def _show_fixtures_per_test(config: Config, session: Session) -> None: - import _pytest.config - - session.perform_collect() - curdir = py.path.local() - tw = _pytest.config.create_terminal_writer(config) - verbose = config.getvalue("verbose") - - def get_best_relpath(func): + import _pytest.config + + session.perform_collect() + curdir = py.path.local() + tw = _pytest.config.create_terminal_writer(config) + verbose = config.getvalue("verbose") + + def get_best_relpath(func): loc = getlocation(func, str(curdir)) return curdir.bestrelpath(py.path.local(loc)) - + def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: - argname = fixture_def.argname - if verbose <= 0 and argname.startswith("_"): - return - if verbose > 0: - bestrel = get_best_relpath(fixture_def.func) + argname = fixture_def.argname + if verbose <= 0 and argname.startswith("_"): + return + if verbose > 0: + bestrel = get_best_relpath(fixture_def.func) funcargspec = f"{argname} -- {bestrel}" - else: - funcargspec = argname - tw.line(funcargspec, green=True) + else: + funcargspec = argname + tw.line(funcargspec, green=True) fixture_doc = inspect.getdoc(fixture_def.func) - if fixture_doc: - write_docstring(tw, fixture_doc) - else: - tw.line(" no docstring available", red=True) - + if fixture_doc: + write_docstring(tw, fixture_doc) + else: + tw.line(" no docstring available", red=True) + def write_item(item: nodes.Item) -> None: # Not all items have _fixtureinfo attribute. info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None) if info is None or not info.name2fixturedefs: # This test item does not use any fixtures. - return - tw.line() + return + tw.line() tw.sep("-", f"fixtures used by {item.name}") # TODO: Fix this type ignore. tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined] # dict key not used in loop but needed for sorting. - for _, fixturedefs in sorted(info.name2fixturedefs.items()): - assert fixturedefs is not None - if not fixturedefs: - continue + for _, fixturedefs in sorted(info.name2fixturedefs.items()): + assert fixturedefs is not None + if not fixturedefs: + continue # Last item is expected to be the one used by the test item. - write_fixture(fixturedefs[-1]) - - for session_item in session.items: - write_item(session_item) - - + write_fixture(fixturedefs[-1]) + + for session_item in session.items: + write_item(session_item) + + def showfixtures(config: Config) -> Union[int, ExitCode]: - from _pytest.main import wrap_session - - return wrap_session(config, _showfixtures_main) - - + from _pytest.main import wrap_session + + return wrap_session(config, _showfixtures_main) + + def _showfixtures_main(config: Config, session: Session) -> None: - import _pytest.config - - session.perform_collect() - curdir = py.path.local() - tw = _pytest.config.create_terminal_writer(config) - verbose = config.getvalue("verbose") - - fm = session._fixturemanager - - available = [] + import _pytest.config + + session.perform_collect() + curdir = py.path.local() + tw = _pytest.config.create_terminal_writer(config) + verbose = config.getvalue("verbose") + + fm = session._fixturemanager + + available = [] seen: Set[Tuple[str, str]] = set() - - for argname, fixturedefs in fm._arg2fixturedefs.items(): - assert fixturedefs is not None - if not fixturedefs: - continue - for fixturedef in fixturedefs: + + for argname, fixturedefs in fm._arg2fixturedefs.items(): + assert fixturedefs is not None + if not fixturedefs: + continue + for fixturedef in fixturedefs: loc = getlocation(fixturedef.func, str(curdir)) - if (fixturedef.argname, loc) in seen: - continue - seen.add((fixturedef.argname, loc)) - available.append( - ( - len(fixturedef.baseid), - fixturedef.func.__module__, + if (fixturedef.argname, loc) in seen: + continue + seen.add((fixturedef.argname, loc)) + available.append( + ( + len(fixturedef.baseid), + fixturedef.func.__module__, curdir.bestrelpath(py.path.local(loc)), - fixturedef.argname, - fixturedef, - ) - ) - - available.sort() - currentmodule = None - for baseid, module, bestrel, argname, fixturedef in available: - if currentmodule != module: - if not module.startswith("_pytest."): - tw.line() + fixturedef.argname, + fixturedef, + ) + ) + + available.sort() + currentmodule = None + for baseid, module, bestrel, argname, fixturedef in available: + if currentmodule != module: + if not module.startswith("_pytest."): + tw.line() tw.sep("-", f"fixtures defined from {module}") - currentmodule = module - if verbose <= 0 and argname[0] == "_": - continue + currentmodule = module + if verbose <= 0 and argname[0] == "_": + continue tw.write(argname, green=True) if fixturedef.scope != "function": tw.write(" [%s scope]" % fixturedef.scope, cyan=True) - if verbose > 0: + if verbose > 0: tw.write(" -- %s" % bestrel, yellow=True) tw.write("\n") loc = getlocation(fixturedef.func, str(curdir)) doc = inspect.getdoc(fixturedef.func) - if doc: - write_docstring(tw, doc) - else: + if doc: + write_docstring(tw, doc) + else: tw.line(f" {loc}: no docstring available", red=True) tw.line() - - + + def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: for line in doc.split("\n"): tw.line(indent + line) - - + + class Function(PyobjMixin, nodes.Item): """An Item responsible for setting up and executing a Python test function. @@ -1573,28 +1573,28 @@ class Function(PyobjMixin, nodes.Item): 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. - _ALLOW_MARKERS = False - - def __init__( - self, + _ALLOW_MARKERS = False + + def __init__( + self, name: str, - parent, + parent, config: Optional[Config] = None, callspec: Optional[CallSpec2] = None, - callobj=NOTSET, - keywords=None, + callobj=NOTSET, + keywords=None, session: Optional[Session] = None, fixtureinfo: Optional[FuncFixtureInfo] = None, originalname: Optional[str] = None, ) -> None: super().__init__(name, parent, config=config, session=session) - if callobj is not NOTSET: - self.obj = callobj - + if callobj is not NOTSET: + self.obj = callobj + #: Original function name, without any decorations (for example #: parametrization adds a ``"[...]"`` suffix to function names), used to access #: the underlying function object from ``parent`` (in case ``callobj`` is not given @@ -1606,21 +1606,21 @@ class Function(PyobjMixin, nodes.Item): # Note: when FunctionDefinition is introduced, we should change ``originalname`` # to a readonly property that returns FunctionDefinition.name. - self.keywords.update(self.obj.__dict__) - self.own_markers.extend(get_unpacked_marks(self.obj)) - if callspec: - self.callspec = callspec - # this is total hostile and a mess - # keywords are broken by design by now - # this will be redeemed later - for mark in callspec.marks: - # feel free to cry, this was broken for years before - # and keywords cant fix it per design - self.keywords[mark.name] = mark - self.own_markers.extend(normalize_mark_list(callspec.marks)) - if keywords: - self.keywords.update(keywords) - + self.keywords.update(self.obj.__dict__) + self.own_markers.extend(get_unpacked_marks(self.obj)) + if callspec: + self.callspec = callspec + # this is total hostile and a mess + # keywords are broken by design by now + # this will be redeemed later + for mark in callspec.marks: + # feel free to cry, this was broken for years before + # and keywords cant fix it per design + self.keywords[mark.name] = mark + self.own_markers.extend(normalize_mark_list(callspec.marks)) + if keywords: + self.keywords.update(keywords) + # todo: this is a hell of a hack # https://github.com/pytest-dev/pytest/issues/4569 @@ -1632,14 +1632,14 @@ class Function(PyobjMixin, nodes.Item): } ) - if fixtureinfo is None: - fixtureinfo = self.session._fixturemanager.getfixtureinfo( + if fixtureinfo is None: + fixtureinfo = self.session._fixturemanager.getfixtureinfo( self, self.obj, self.cls, funcargs=True - ) + ) self._fixtureinfo: FuncFixtureInfo = fixtureinfo - self.fixturenames = fixtureinfo.names_closure - self._initrequest() - + self.fixturenames = fixtureinfo.names_closure + self._initrequest() + @classmethod def from_parent(cls, parent, **kw): # todo: determine sound type limitations """The public constructor.""" @@ -1648,31 +1648,31 @@ class Function(PyobjMixin, nodes.Item): def _initrequest(self) -> None: self.funcargs: Dict[str, object] = {} self._request = fixtures.FixtureRequest(self, _ispytest=True) - - @property - def function(self): + + @property + def function(self): """Underlying python 'function' object.""" - return getimfunc(self.obj) - - def _getobj(self): + return getimfunc(self.obj) + + def _getobj(self): assert self.parent is not None return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined] - - @property - def _pyfuncitem(self): + + @property + def _pyfuncitem(self): """(compatonly) for code expecting pytest-2.2 style request objects.""" - return self - + return self + def runtest(self) -> None: """Execute the underlying test function.""" - self.ihook.pytest_pyfunc_call(pyfuncitem=self) - + self.ihook.pytest_pyfunc_call(pyfuncitem=self) + def setup(self) -> None: if isinstance(self.parent, Instance): self.parent.newinstance() self.obj = self._getobj() self._request._fillfixtures() - + def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): code = _pytest._code.Code.from_function(get_real_func(self.obj)) @@ -1685,7 +1685,7 @@ class Function(PyobjMixin, nodes.Item): ntraceback = ntraceback.filter(filter_traceback) if not ntraceback: ntraceback = traceback - + excinfo.traceback = ntraceback.filter() # issue364: mark all but first and last frames to # only show a single-line message for each frame. @@ -1704,13 +1704,13 @@ class Function(PyobjMixin, nodes.Item): return self._repr_failure_py(excinfo, style=style) -class FunctionDefinition(Function): - """ +class FunctionDefinition(Function): + """ This class is a step gap solution until we evolve to have actual function definition nodes and manage to get rid of ``metafunc``. - """ - + """ + def runtest(self) -> None: raise RuntimeError("function definitions are not supposed to be run as tests") - - setup = runtest + + setup = runtest diff --git a/contrib/python/pytest/py3/_pytest/python_api.py b/contrib/python/pytest/py3/_pytest/python_api.py index 79ff4800b5..81ce4f8953 100644 --- a/contrib/python/pytest/py3/_pytest/python_api.py +++ b/contrib/python/pytest/py3/_pytest/python_api.py @@ -1,9 +1,9 @@ -import math -import pprint +import math +import pprint from collections.abc import Iterable from collections.abc import Mapping from collections.abc import Sized -from decimal import Decimal +from decimal import Decimal from numbers import Complex from types import TracebackType from typing import Any @@ -18,210 +18,210 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - + if TYPE_CHECKING: from numpy import ndarray - -import _pytest._code + +import _pytest._code from _pytest.compat import final -from _pytest.compat import STRING_TYPES -from _pytest.outcomes import fail - - +from _pytest.compat import STRING_TYPES +from _pytest.outcomes import fail + + def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: at_str = f" at {at}" if at else "" - return TypeError( - "cannot make approximate comparisons to non-numeric values: {!r} {}".format( - value, at_str - ) - ) - - -# builtin pytest.approx helper - - + return TypeError( + "cannot make approximate comparisons to non-numeric values: {!r} {}".format( + value, at_str + ) + ) + + +# builtin pytest.approx helper + + class ApproxBase: """Provide shared utilities for making approximate comparisons between numbers or sequences of numbers.""" - - # Tell numpy to use our `__eq__` operator instead of its. - __array_ufunc__ = None - __array_priority__ = 100 - + + # Tell numpy to use our `__eq__` operator instead of its. + __array_ufunc__ = None + __array_priority__ = 100 + def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None: - __tracebackhide__ = True - self.expected = expected - self.abs = abs - self.rel = rel - self.nan_ok = nan_ok - self._check_type() - + __tracebackhide__ = True + self.expected = expected + self.abs = abs + self.rel = rel + self.nan_ok = nan_ok + self._check_type() + def __repr__(self) -> str: - raise NotImplementedError - + raise NotImplementedError + def __eq__(self, actual) -> bool: - return all( - a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) - ) - + return all( + a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) + ) + # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore - + def __ne__(self, actual) -> bool: - return not (actual == self) - + return not (actual == self) + def _approx_scalar(self, x) -> "ApproxScalar": - return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) - - def _yield_comparisons(self, actual): + return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) + + def _yield_comparisons(self, actual): """Yield all the pairs of numbers to be compared. This is used to implement the `__eq__` method. - """ - raise NotImplementedError - + """ + raise NotImplementedError + def _check_type(self) -> None: """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 - # other case, the approx() function ensures that the expected value has - # a numeric type. For this reason, the default is to do nothing. The - # classes that deal with sequences should reimplement this method to - # raise if there are any non-numeric elements in the sequence. - pass - - -def _recursive_list_map(f, x): - if isinstance(x, list): - return list(_recursive_list_map(f, xi) for xi in x) - else: - return f(x) - - -class ApproxNumpy(ApproxBase): + # 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 + # a numeric type. For this reason, the default is to do nothing. The + # classes that deal with sequences should reimplement this method to + # raise if there are any non-numeric elements in the sequence. + pass + + +def _recursive_list_map(f, x): + if isinstance(x, list): + return list(_recursive_list_map(f, xi) for xi in x) + else: + return f(x) + + +class ApproxNumpy(ApproxBase): """Perform approximate comparisons where the expected value is numpy array.""" - + 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()) return f"approx({list_scalars!r})" - + def __eq__(self, actual) -> bool: - import numpy as np - + import numpy as np + # self.expected is supposed to always be an array here. - - if not np.isscalar(actual): - try: - actual = np.asarray(actual) + + if not np.isscalar(actual): + try: + actual = np.asarray(actual) except Exception as e: raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e - - if not np.isscalar(actual) and actual.shape != self.expected.shape: - return False - - return ApproxBase.__eq__(self, actual) - - def _yield_comparisons(self, actual): - import numpy as np - - # `actual` can either be a numpy array or a scalar, it is treated in - # `__eq__` before being passed to `ApproxBase.__eq__`, which is the - # only method that calls this one. - - if np.isscalar(actual): - for i in np.ndindex(self.expected.shape): + + if not np.isscalar(actual) and actual.shape != self.expected.shape: + return False + + return ApproxBase.__eq__(self, actual) + + def _yield_comparisons(self, actual): + import numpy as np + + # `actual` can either be a numpy array or a scalar, it is treated in + # `__eq__` before being passed to `ApproxBase.__eq__`, which is the + # only method that calls this one. + + if np.isscalar(actual): + for i in np.ndindex(self.expected.shape): yield actual, self.expected[i].item() - else: - for i in np.ndindex(self.expected.shape): + else: + for i in np.ndindex(self.expected.shape): yield actual[i].item(), self.expected[i].item() - - -class ApproxMapping(ApproxBase): + + +class ApproxMapping(ApproxBase): """Perform approximate comparisons where the expected value is a mapping with numeric values (the keys can be anything).""" - + def __repr__(self) -> str: - return "approx({!r})".format( - {k: self._approx_scalar(v) for k, v in self.expected.items()} - ) - + return "approx({!r})".format( + {k: self._approx_scalar(v) for k, v in self.expected.items()} + ) + def __eq__(self, actual) -> bool: try: if set(actual.keys()) != set(self.expected.keys()): return False except AttributeError: - return False - - return ApproxBase.__eq__(self, actual) - - def _yield_comparisons(self, actual): - for k in self.expected.keys(): - yield actual[k], self.expected[k] - + return False + + return ApproxBase.__eq__(self, actual) + + def _yield_comparisons(self, actual): + for k in self.expected.keys(): + yield actual[k], self.expected[k] + def _check_type(self) -> None: - __tracebackhide__ = True - for key, value in self.expected.items(): - if isinstance(value, type(self.expected)): - msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}" - raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) - - + __tracebackhide__ = True + for key, value in self.expected.items(): + if isinstance(value, type(self.expected)): + msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}" + raise TypeError(msg.format(key, value, pprint.pformat(self.expected))) + + class ApproxSequencelike(ApproxBase): """Perform approximate comparisons where the expected value is a sequence of numbers.""" - + def __repr__(self) -> str: - seq_type = type(self.expected) - if seq_type not in (tuple, list, set): - seq_type = list - return "approx({!r})".format( - seq_type(self._approx_scalar(x) for x in self.expected) - ) - + seq_type = type(self.expected) + if seq_type not in (tuple, list, set): + seq_type = list + return "approx({!r})".format( + seq_type(self._approx_scalar(x) for x in self.expected) + ) + def __eq__(self, actual) -> bool: try: if len(actual) != len(self.expected): return False except TypeError: - return False - return ApproxBase.__eq__(self, actual) - - def _yield_comparisons(self, actual): - return zip(actual, self.expected) - + return False + return ApproxBase.__eq__(self, actual) + + def _yield_comparisons(self, actual): + return zip(actual, self.expected) + def _check_type(self) -> None: - __tracebackhide__ = True - for index, x in enumerate(self.expected): - if isinstance(x, type(self.expected)): - msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}" - raise TypeError(msg.format(x, index, pprint.pformat(self.expected))) - - -class ApproxScalar(ApproxBase): + __tracebackhide__ = True + for index, x in enumerate(self.expected): + if isinstance(x, type(self.expected)): + msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}" + raise TypeError(msg.format(x, index, pprint.pformat(self.expected))) + + +class ApproxScalar(ApproxBase): """Perform approximate comparisons where the expected value is a single number.""" - + # Using Real should be better than this Union, but not possible yet: # https://github.com/python/typeshed/pull/3108 DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12 DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6 - + def __repr__(self) -> str: """Return a string communicating both the expected value and the tolerance for the comparison being made. For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠±180°``. - """ - + """ + # Don't show a tolerance for values that aren't compared using # tolerances, i.e. non-numerics and infinities. Need to call abs to # handle complex numbers, e.g. (inf + 1j). if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf( abs(self.expected) # type: ignore[arg-type] ): - return str(self.expected) - - # If a sensible tolerance can't be calculated, self.tolerance will - # raise a ValueError. In this case, display '???'. - try: + return str(self.expected) + + # If a sensible tolerance can't be calculated, self.tolerance will + # raise a ValueError. In this case, display '???'. + try: vetted_tolerance = f"{self.tolerance:.1e}" if ( isinstance(self.expected, Complex) @@ -229,24 +229,24 @@ class ApproxScalar(ApproxBase): and not math.isinf(self.tolerance) ): vetted_tolerance += " ∠±180°" - except ValueError: - vetted_tolerance = "???" - + except ValueError: + vetted_tolerance = "???" + return f"{self.expected} ± {vetted_tolerance}" - + def __eq__(self, actual) -> bool: """Return whether the given value is equal to the expected value within the pre-specified tolerance.""" asarray = _as_numpy_array(actual) if asarray is not None: - # Call ``__eq__()`` manually to prevent infinite-recursion with - # numpy<1.13. See #3748. + # Call ``__eq__()`` manually to prevent infinite-recursion with + # numpy<1.13. See #3748. return all(self.__eq__(a) for a in asarray.flat) - - # Short-circuit exact equality. - if actual == self.expected: - return True - + + # Short-circuit exact equality. + if actual == self.expected: + return True + # If either type is non-numeric, fall back to strict equality. # NB: we need Complex, rather than just Number, to ensure that __abs__, # __sub__, and __float__ are defined. @@ -256,176 +256,176 @@ class ApproxScalar(ApproxBase): ): return False - # Allow the user to control whether NaNs are considered equal to each - # other or not. The abs() calls are for compatibility with complex - # numbers. + # Allow the user to control whether NaNs are considered equal to each + # other or not. The abs() calls are for compatibility with complex + # numbers. if math.isnan(abs(self.expected)): # type: ignore[arg-type] return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type] - - # Infinity shouldn't be approximately equal to anything but itself, but - # if there's a relative tolerance, it will be infinite and infinity - # will seem approximately equal to everything. The equal-to-itself - # case would have been short circuited above, so here we can just - # return false if the expected value is infinite. The abs() call is - # for compatibility with complex numbers. + + # Infinity shouldn't be approximately equal to anything but itself, but + # if there's a relative tolerance, it will be infinite and infinity + # will seem approximately equal to everything. The equal-to-itself + # case would have been short circuited above, so here we can just + # return false if the expected value is infinite. The abs() call is + # for compatibility with complex numbers. if math.isinf(abs(self.expected)): # type: ignore[arg-type] - return False - - # Return true if the two numbers are within the tolerance. + return False + + # Return true if the two numbers are within the tolerance. result: bool = abs(self.expected - actual) <= self.tolerance return result - + # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore - - @property - def tolerance(self): + + @property + def tolerance(self): """Return the tolerance for the comparison. This could be either an absolute tolerance or a relative tolerance, depending on what the user specified or which would be larger. - """ - - def set_default(x, default): - return x if x is not None else default - - # Figure out what the absolute tolerance should be. ``self.abs`` is - # either None or a value specified by the user. - absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE) - - if absolute_tolerance < 0: - raise ValueError( + """ + + def set_default(x, default): + return x if x is not None else default + + # Figure out what the absolute tolerance should be. ``self.abs`` is + # either None or a value specified by the user. + absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE) + + if absolute_tolerance < 0: + raise ValueError( f"absolute tolerance can't be negative: {absolute_tolerance}" - ) - if math.isnan(absolute_tolerance): - raise ValueError("absolute tolerance can't be NaN.") - - # If the user specified an absolute tolerance but not a relative one, - # just return the absolute tolerance. - if self.rel is None: - if self.abs is not None: - return absolute_tolerance - - # Figure out what the relative tolerance should be. ``self.rel`` is - # either None or a value specified by the user. This is done after - # we've made sure the user didn't ask for an absolute tolerance only, - # because we don't want to raise errors about the relative tolerance if - # we aren't even going to use it. - relative_tolerance = set_default( - self.rel, self.DEFAULT_RELATIVE_TOLERANCE - ) * abs(self.expected) - - if relative_tolerance < 0: - raise ValueError( + ) + if math.isnan(absolute_tolerance): + raise ValueError("absolute tolerance can't be NaN.") + + # If the user specified an absolute tolerance but not a relative one, + # just return the absolute tolerance. + if self.rel is None: + if self.abs is not None: + return absolute_tolerance + + # Figure out what the relative tolerance should be. ``self.rel`` is + # either None or a value specified by the user. This is done after + # we've made sure the user didn't ask for an absolute tolerance only, + # because we don't want to raise errors about the relative tolerance if + # we aren't even going to use it. + relative_tolerance = set_default( + self.rel, self.DEFAULT_RELATIVE_TOLERANCE + ) * abs(self.expected) + + if relative_tolerance < 0: + raise ValueError( f"relative tolerance can't be negative: {absolute_tolerance}" - ) - if math.isnan(relative_tolerance): - raise ValueError("relative tolerance can't be NaN.") - - # Return the larger of the relative and absolute tolerances. - return max(relative_tolerance, absolute_tolerance) - - -class ApproxDecimal(ApproxScalar): + ) + if math.isnan(relative_tolerance): + raise ValueError("relative tolerance can't be NaN.") + + # Return the larger of the relative and absolute tolerances. + return max(relative_tolerance, absolute_tolerance) + + +class ApproxDecimal(ApproxScalar): """Perform approximate comparisons where the expected value is a Decimal.""" - - DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") - DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") - - + + DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") + DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") + + 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 - within some tolerance. - - Due to the `intricacies of floating-point arithmetic`__, numbers that we - would intuitively expect to be equal are not always so:: - - >>> 0.1 + 0.2 == 0.3 - False - - __ https://docs.python.org/3/tutorial/floatingpoint.html - - This problem is commonly encountered when writing tests, e.g. when making - sure that floating-point values are what you expect them to be. One way to - deal with this problem is to assert that two floating-point numbers are - equal to within some appropriate tolerance:: - - >>> abs((0.1 + 0.2) - 0.3) < 1e-6 - True - - However, comparisons like this are tedious to write and difficult to - understand. Furthermore, absolute comparisons like the one above are - usually discouraged because there's no tolerance that works well for all - situations. ``1e-6`` is good for numbers around ``1``, but too small for - very big numbers and too big for very small ones. It's better to express - the tolerance as a fraction of the expected value, but relative comparisons - like that are even more difficult to write correctly and concisely. - - The ``approx`` class performs floating-point comparisons using a syntax - that's as intuitive as possible:: - - >>> from pytest import approx - >>> 0.1 + 0.2 == approx(0.3) - True - - The same syntax also works for sequences of numbers:: - - >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6)) - True - - Dictionary *values*:: - - >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6}) - True - - ``numpy`` arrays:: - - >>> import numpy as np # doctest: +SKIP - >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP - True - - And for a ``numpy`` array against a scalar:: - - >>> import numpy as np # doctest: +SKIP - >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP - True - - By default, ``approx`` considers numbers within a relative tolerance of - ``1e-6`` (i.e. one part in a million) of its expected value to be equal. - This treatment would lead to surprising results if the expected value was - ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``. - To handle this case less surprisingly, ``approx`` also considers numbers - within an absolute tolerance of ``1e-12`` of its expected value to be - equal. Infinity and NaN are special cases. Infinity is only considered - equal to itself, regardless of the relative tolerance. NaN is not - considered equal to anything by default, but you can make it be equal to - itself by setting the ``nan_ok`` argument to True. (This is meant to - facilitate comparing arrays that use NaN to mean "no data".) - - Both the relative and absolute tolerances can be changed by passing - arguments to the ``approx`` constructor:: - - >>> 1.0001 == approx(1) - False - >>> 1.0001 == approx(1, rel=1e-3) - True - >>> 1.0001 == approx(1, abs=1e-3) - True - - If you specify ``abs`` but not ``rel``, the comparison will not consider - the relative tolerance at all. In other words, two numbers that are within - the default relative tolerance of ``1e-6`` will still be considered unequal - if they exceed the specified absolute tolerance. If you specify both - ``abs`` and ``rel``, the numbers will be considered equal if either - tolerance is met:: - - >>> 1 + 1e-8 == approx(1) - True - >>> 1 + 1e-8 == approx(1, abs=1e-12) - False - >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12) - True - + within some tolerance. + + Due to the `intricacies of floating-point arithmetic`__, numbers that we + would intuitively expect to be equal are not always so:: + + >>> 0.1 + 0.2 == 0.3 + False + + __ https://docs.python.org/3/tutorial/floatingpoint.html + + This problem is commonly encountered when writing tests, e.g. when making + sure that floating-point values are what you expect them to be. One way to + deal with this problem is to assert that two floating-point numbers are + equal to within some appropriate tolerance:: + + >>> abs((0.1 + 0.2) - 0.3) < 1e-6 + True + + However, comparisons like this are tedious to write and difficult to + understand. Furthermore, absolute comparisons like the one above are + usually discouraged because there's no tolerance that works well for all + situations. ``1e-6`` is good for numbers around ``1``, but too small for + very big numbers and too big for very small ones. It's better to express + the tolerance as a fraction of the expected value, but relative comparisons + like that are even more difficult to write correctly and concisely. + + The ``approx`` class performs floating-point comparisons using a syntax + that's as intuitive as possible:: + + >>> from pytest import approx + >>> 0.1 + 0.2 == approx(0.3) + True + + The same syntax also works for sequences of numbers:: + + >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6)) + True + + Dictionary *values*:: + + >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6}) + True + + ``numpy`` arrays:: + + >>> import numpy as np # doctest: +SKIP + >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP + True + + And for a ``numpy`` array against a scalar:: + + >>> import numpy as np # doctest: +SKIP + >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP + True + + By default, ``approx`` considers numbers within a relative tolerance of + ``1e-6`` (i.e. one part in a million) of its expected value to be equal. + This treatment would lead to surprising results if the expected value was + ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``. + To handle this case less surprisingly, ``approx`` also considers numbers + within an absolute tolerance of ``1e-12`` of its expected value to be + equal. Infinity and NaN are special cases. Infinity is only considered + equal to itself, regardless of the relative tolerance. NaN is not + considered equal to anything by default, but you can make it be equal to + itself by setting the ``nan_ok`` argument to True. (This is meant to + facilitate comparing arrays that use NaN to mean "no data".) + + Both the relative and absolute tolerances can be changed by passing + arguments to the ``approx`` constructor:: + + >>> 1.0001 == approx(1) + False + >>> 1.0001 == approx(1, rel=1e-3) + True + >>> 1.0001 == approx(1, abs=1e-3) + True + + If you specify ``abs`` but not ``rel``, the comparison will not consider + the relative tolerance at all. In other words, two numbers that are within + the default relative tolerance of ``1e-6`` will still be considered unequal + if they exceed the specified absolute tolerance. If you specify both + ``abs`` and ``rel``, the numbers will be considered equal if either + tolerance is met:: + + >>> 1 + 1e-8 == approx(1) + True + >>> 1 + 1e-8 == approx(1, abs=1e-12) + False + >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12) + True + You can also use ``approx`` to compare nonnumeric types, or dicts and sequences containing nonnumeric types, in which case it falls back to strict equality. This can be useful for comparing dicts and sequences that @@ -438,63 +438,63 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: >>> ["foo", 1.0000005] == approx([None,1]) False - If you're thinking about using ``approx``, then you might want to know how - it compares to other good ways of comparing floating-point numbers. All of - these algorithms are based on relative and absolute tolerances and should - agree for the most part, but they do have meaningful differences: - - - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative - tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute - tolerance is met. Because the relative tolerance is calculated w.r.t. - both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor - ``b`` is a "reference value"). You have to specify an absolute tolerance - if you want to compare to ``0.0`` because there is no tolerance by + If you're thinking about using ``approx``, then you might want to know how + it compares to other good ways of comparing floating-point numbers. All of + these algorithms are based on relative and absolute tolerances and should + agree for the most part, but they do have meaningful differences: + + - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative + tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute + tolerance is met. Because the relative tolerance is calculated w.r.t. + both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor + ``b`` is a "reference value"). You have to specify an absolute tolerance + if you want to compare to ``0.0`` because there is no tolerance by default. `More information...`__ - - __ https://docs.python.org/3/library/math.html#math.isclose - - - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference - between ``a`` and ``b`` is less that the sum of the relative tolerance - w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance - is only calculated w.r.t. ``b``, this test is asymmetric and you can - think of ``b`` as the reference value. Support for comparing sequences - is provided by ``numpy.allclose``. `More information...`__ - + + __ https://docs.python.org/3/library/math.html#math.isclose + + - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference + between ``a`` and ``b`` is less that the sum of the relative tolerance + w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance + is only calculated w.r.t. ``b``, this test is asymmetric and you can + think of ``b`` as the reference value. Support for comparing sequences + is provided by ``numpy.allclose``. `More information...`__ + __ https://numpy.org/doc/stable/reference/generated/numpy.isclose.html - - - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b`` - are within an absolute tolerance of ``1e-7``. No relative tolerance is - considered and the absolute tolerance cannot be changed, so this function - is not appropriate for very large or very small numbers. Also, it's only - available in subclasses of ``unittest.TestCase`` and it's ugly because it - doesn't follow PEP8. `More information...`__ - + + - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b`` + are within an absolute tolerance of ``1e-7``. No relative tolerance is + considered and the absolute tolerance cannot be changed, so this function + is not appropriate for very large or very small numbers. Also, it's only + available in subclasses of ``unittest.TestCase`` and it's ugly because it + doesn't follow PEP8. `More information...`__ + __ https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertAlmostEqual - - - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative - tolerance is met w.r.t. ``b`` or if the absolute tolerance is met. - Because the relative tolerance is only calculated w.r.t. ``b``, this test - is asymmetric and you can think of ``b`` as the reference value. In the - special case that you explicitly specify an absolute tolerance but not a - relative tolerance, only the absolute tolerance is considered. - - .. warning:: - - .. versionchanged:: 3.2 - - In order to avoid inconsistent behavior, ``TypeError`` is - raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons. - The example below illustrates the problem:: - - assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10) - assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10) - - In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)`` - to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to - comparison. This is because the call hierarchy of rich comparisons - follows a fixed behavior. `More information...`__ - - __ https://docs.python.org/3/reference/datamodel.html#object.__ge__ + + - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative + tolerance is met w.r.t. ``b`` or if the absolute tolerance is met. + Because the relative tolerance is only calculated w.r.t. ``b``, this test + is asymmetric and you can think of ``b`` as the reference value. In the + special case that you explicitly specify an absolute tolerance but not a + relative tolerance, only the absolute tolerance is considered. + + .. warning:: + + .. versionchanged:: 3.2 + + In order to avoid inconsistent behavior, ``TypeError`` is + raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons. + The example below illustrates the problem:: + + assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10) + assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10) + + In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)`` + to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to + comparison. This is because the call hierarchy of rich comparisons + follows a fixed behavior. `More information...`__ + + __ https://docs.python.org/3/reference/datamodel.html#object.__ge__ .. versionchanged:: 3.7.1 ``approx`` raises ``TypeError`` when it encounters a dict value or @@ -503,32 +503,32 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: .. versionchanged:: 6.1.0 ``approx`` falls back to strict equality for nonnumeric types instead of raising ``TypeError``. - """ - - # Delegate the comparison to a class that knows how to deal with the type - # of the expected value (e.g. int, float, list, dict, numpy.array, etc). - # - # The primary responsibility of these classes is to implement ``__eq__()`` - # and ``__repr__()``. The former is used to actually check if some - # "actual" value is equivalent to the given expected value within the - # allowed tolerance. The latter is used to show the user the expected - # value and tolerance, in the case that a test failed. - # - # The actual logic for making approximate comparisons can be found in - # ApproxScalar, which is used to compare individual numbers. All of the - # other Approx classes eventually delegate to this class. The ApproxBase - # class provides some convenient methods and overloads, but isn't really - # essential. - - __tracebackhide__ = True - - if isinstance(expected, Decimal): + """ + + # Delegate the comparison to a class that knows how to deal with the type + # of the expected value (e.g. int, float, list, dict, numpy.array, etc). + # + # The primary responsibility of these classes is to implement ``__eq__()`` + # and ``__repr__()``. The former is used to actually check if some + # "actual" value is equivalent to the given expected value within the + # allowed tolerance. The latter is used to show the user the expected + # value and tolerance, in the case that a test failed. + # + # The actual logic for making approximate comparisons can be found in + # ApproxScalar, which is used to compare individual numbers. All of the + # other Approx classes eventually delegate to this class. The ApproxBase + # class provides some convenient methods and overloads, but isn't really + # essential. + + __tracebackhide__ = True + + if isinstance(expected, Decimal): cls: Type[ApproxBase] = ApproxDecimal - elif isinstance(expected, Mapping): - cls = ApproxMapping - elif _is_numpy_array(expected): + elif isinstance(expected, Mapping): + cls = ApproxMapping + elif _is_numpy_array(expected): expected = _as_numpy_array(expected) - cls = ApproxNumpy + cls = ApproxNumpy elif ( isinstance(expected, Iterable) and isinstance(expected, Sized) @@ -536,17 +536,17 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable] ): cls = ApproxSequencelike - else: + else: cls = ApproxScalar - - return cls(expected, rel, abs, nan_ok) - - + + return cls(expected, rel, abs, nan_ok) + + def _is_numpy_array(obj: object) -> bool: - """ + """ Return true if the given object is implicitly convertible to ndarray, and numpy is already imported. - """ + """ return _as_numpy_array(obj) is not None @@ -555,10 +555,10 @@ def _as_numpy_array(obj: object) -> Optional["ndarray"]: Return an ndarray if the given object is implicitly convertible to ndarray, and numpy is already imported, otherwise None. """ - import sys - + import sys + np: Any = sys.modules.get("numpy") - if np is not None: + if np is not None: # avoid infinite recursion on numpy scalars, which have __array__ if np.isscalar(obj): return None @@ -567,12 +567,12 @@ def _as_numpy_array(obj: object) -> Optional["ndarray"]: elif hasattr(obj, "__array__") or hasattr("obj", "__array_interface__"): return np.asarray(obj) return None - - -# builtin pytest.raises helper - + + +# builtin pytest.raises helper + _E = TypeVar("_E", bound=BaseException) - + @overload def raises( @@ -598,21 +598,21 @@ def raises( ) -> Union["RaisesContext[_E]", _pytest._code.ExceptionInfo[_E]]: r"""Assert that a code block/function call raises ``expected_exception`` or raise a failure exception otherwise. - + :kwparam match: If specified, a string containing a regular expression, or a regular expression object, that is tested against the string representation of the exception using ``re.search``. To match a literal string that may contain `special characters`__, the pattern can first be escaped with ``re.escape``. - + (This is only used when ``pytest.raises`` is used as a context manager, and passed through to the function otherwise. When using ``pytest.raises`` as a function, you can use: ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.) - + __ https://docs.python.org/3/library/re.html#regular-expression-syntax - + .. currentmodule:: _pytest._code Use ``pytest.raises`` as a context manager, which will capture the exception of the given @@ -620,17 +620,17 @@ def raises( >>> import pytest >>> with pytest.raises(ZeroDivisionError): - ... 1/0 - + ... 1/0 + If the code block does not raise the expected exception (``ZeroDivisionError`` in the example above), or no exception at all, the check will fail instead. - + You can also use the keyword argument ``match`` to assert that the exception matches a text or regex:: - + >>> with pytest.raises(ValueError, match='must be 0 or None'): ... raise ValueError("value must be 0 or None") - + >>> with pytest.raises(ValueError, match=r'must be \d+$'): ... raise ValueError("value must be 42") @@ -642,72 +642,72 @@ def raises( >>> assert exc_info.type is ValueError >>> assert exc_info.value.args[0] == "value must be 42" - .. note:: - - When using ``pytest.raises`` as a context manager, it's worthwhile to - note that normal context manager rules apply and that the exception - raised *must* be the final line in the scope of the context manager. - Lines of code after that, within the scope of the context manager will - not be executed. For example:: - - >>> value = 15 + .. note:: + + When using ``pytest.raises`` as a context manager, it's worthwhile to + note that normal context manager rules apply and that the exception + raised *must* be the final line in the scope of the context manager. + Lines of code after that, within the scope of the context manager will + not be executed. For example:: + + >>> value = 15 >>> with pytest.raises(ValueError) as exc_info: - ... if value > 10: - ... raise ValueError("value must be <= 10") + ... if value > 10: + ... raise ValueError("value must be <= 10") ... assert exc_info.type is ValueError # this will not execute - - Instead, the following approach must be taken (note the difference in - scope):: - + + Instead, the following approach must be taken (note the difference in + scope):: + >>> with pytest.raises(ValueError) as exc_info: - ... if value > 10: - ... raise ValueError("value must be <= 10") - ... + ... if value > 10: + ... raise ValueError("value must be <= 10") + ... >>> assert exc_info.type is ValueError - + **Using with** ``pytest.mark.parametrize`` - + When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests such that some runs raise an exception and others do not. - + See :ref:`parametrizing_conditional_raising` for an example. - + **Legacy form** - - It is possible to specify a callable by passing a to-be-called lambda:: - - >>> raises(ZeroDivisionError, lambda: 1/0) - <ExceptionInfo ...> - - or you can specify an arbitrary callable with arguments:: - - >>> def f(x): return 1/x - ... - >>> raises(ZeroDivisionError, f, 0) - <ExceptionInfo ...> - >>> raises(ZeroDivisionError, f, x=0) - <ExceptionInfo ...> - + + It is possible to specify a callable by passing a to-be-called lambda:: + + >>> raises(ZeroDivisionError, lambda: 1/0) + <ExceptionInfo ...> + + or you can specify an arbitrary callable with arguments:: + + >>> def f(x): return 1/x + ... + >>> raises(ZeroDivisionError, f, 0) + <ExceptionInfo ...> + >>> raises(ZeroDivisionError, f, x=0) + <ExceptionInfo ...> + The form above is fully supported but discouraged for new code because the context manager form is regarded as more readable and less error-prone. - - .. note:: - Similar to caught exception objects in Python, explicitly clearing - local references to returned ``ExceptionInfo`` objects can - help the Python interpreter speed up its garbage collection. - - Clearing those references breaks a reference cycle - (``ExceptionInfo`` --> caught exception --> frame stack raising - the exception --> current frame stack --> local variables --> - ``ExceptionInfo``) which makes Python keep all objects referenced - from that cycle (including all local variables in the current + + .. note:: + Similar to caught exception objects in Python, explicitly clearing + local references to returned ``ExceptionInfo`` objects can + help the Python interpreter speed up its garbage collection. + + Clearing those references breaks a reference cycle + (``ExceptionInfo`` --> caught exception --> frame stack raising + the exception --> current frame stack --> local variables --> + ``ExceptionInfo``) which makes Python keep all objects referenced + from that cycle (including all local variables in the current frame) alive until the next cyclic garbage collection run. More detailed information can be found in the official Python documentation for :ref:`the try statement <python:try>`. - """ - __tracebackhide__ = True - + """ + __tracebackhide__ = True + if isinstance(expected_exception, type): excepted_exceptions: Tuple[Type[_E], ...] = (expected_exception,) else: @@ -717,38 +717,38 @@ def raises( msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable] not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__ raise TypeError(msg.format(not_a)) - + message = f"DID NOT RAISE {expected_exception}" - if not args: + if not args: match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None) - if kwargs: - msg = "Unexpected keyword arguments passed to pytest.raises: " + if kwargs: + msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) msg += "\nUse context-manager form instead?" - raise TypeError(msg) + raise TypeError(msg) return RaisesContext(expected_exception, message, match) - else: - func = args[0] + else: + func = args[0] if not callable(func): raise TypeError( "{!r} object (type: {}) must be callable".format(func, type(func)) ) - try: - func(*args[1:], **kwargs) + try: + func(*args[1:], **kwargs) except expected_exception as e: # We just caught the exception - there is a traceback. assert e.__traceback__ is not None return _pytest._code.ExceptionInfo.from_exc_info( (type(e), e, e.__traceback__) ) - fail(message) - - + fail(message) + + # This doesn't work with mypy for now. Use fail.Exception instead. raises.Exception = fail.Exception # type: ignore - - + + @final class RaisesContext(Generic[_E]): def __init__( @@ -757,24 +757,24 @@ class RaisesContext(Generic[_E]): message: str, match_expr: Optional[Union[str, Pattern[str]]] = None, ) -> None: - self.expected_exception = expected_exception - self.message = message - self.match_expr = match_expr + self.expected_exception = expected_exception + self.message = message + self.match_expr = match_expr self.excinfo: Optional[_pytest._code.ExceptionInfo[_E]] = None - + def __enter__(self) -> _pytest._code.ExceptionInfo[_E]: self.excinfo = _pytest._code.ExceptionInfo.for_later() - return self.excinfo - + return self.excinfo + def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> bool: - __tracebackhide__ = True + __tracebackhide__ = True if exc_type is None: - fail(self.message) + fail(self.message) assert self.excinfo is not None if not issubclass(exc_type, self.expected_exception): return False @@ -782,5 +782,5 @@ class RaisesContext(Generic[_E]): exc_info = cast(Tuple[Type[_E], _E, TracebackType], (exc_type, exc_val, exc_tb)) self.excinfo.fill_unfilled(exc_info) if self.match_expr is not None: - self.excinfo.match(self.match_expr) + self.excinfo.match(self.match_expr) return True diff --git a/contrib/python/pytest/py3/_pytest/recwarn.py b/contrib/python/pytest/py3/_pytest/recwarn.py index 3664113393..d872d9da40 100644 --- a/contrib/python/pytest/py3/_pytest/recwarn.py +++ b/contrib/python/pytest/py3/_pytest/recwarn.py @@ -1,6 +1,6 @@ """Record warnings during test function execution.""" -import re -import warnings +import re +import warnings from types import TracebackType from typing import Any from typing import Callable @@ -14,35 +14,35 @@ from typing import Tuple from typing import Type from typing import TypeVar from typing import Union - + from _pytest.compat import final from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture -from _pytest.outcomes import fail - - +from _pytest.outcomes import fail + + T = TypeVar("T") @fixture def recwarn() -> Generator["WarningsRecorder", None, None]: - """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. - - See http://docs.python.org/library/warnings.html for information - on warning categories. - """ + """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. + + See http://docs.python.org/library/warnings.html for information + on warning categories. + """ wrec = WarningsRecorder(_ispytest=True) - with wrec: - warnings.simplefilter("default") - yield wrec - - + with wrec: + warnings.simplefilter("default") + yield wrec + + @overload def deprecated_call( *, match: Optional[Union[str, Pattern[str]]] = ... ) -> "WarningsRecorder": ... - + @overload def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: @@ -56,15 +56,15 @@ def deprecated_call( This function can be used as a context manager:: - >>> import warnings - >>> def api_call_v2(): - ... warnings.warn('use v3 of this api', DeprecationWarning) - ... return 200 - + >>> import warnings + >>> def api_call_v2(): + ... warnings.warn('use v3 of this api', DeprecationWarning) + ... return 200 + >>> import pytest >>> with pytest.deprecated_call(): - ... assert api_call_v2() == 200 - + ... assert api_call_v2() == 200 + It can also be used by passing a function and ``*args`` and ``**kwargs``, in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings types above. The return value is the return value of the function. @@ -74,13 +74,13 @@ def deprecated_call( The context manager produces a list of :class:`warnings.WarningMessage` objects, one for each warning raised. - """ - __tracebackhide__ = True - if func is not None: - args = (func,) + args - return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) - - + """ + __tracebackhide__ = True + if func is not None: + args = (func,) + args + return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) + + @overload def warns( expected_warning: Optional[Union[Type[Warning], Tuple[Type[Warning], ...]]], @@ -106,130 +106,130 @@ def warns( match: Optional[Union[str, Pattern[str]]] = None, **kwargs: Any, ) -> Union["WarningsChecker", Any]: - r"""Assert that code raises a particular class of warning. - - Specifically, the parameter ``expected_warning`` can be a warning class or - sequence of warning classes, and the inside the ``with`` block must issue a warning of that class or - classes. - - This helper produces a list of :class:`warnings.WarningMessage` objects, - one for each warning raised. - - This function can be used as a context manager, or any of the other ways + r"""Assert that code raises a particular class of warning. + + Specifically, the parameter ``expected_warning`` can be a warning class or + sequence of warning classes, and the inside the ``with`` block must issue a warning of that class or + classes. + + This helper produces a list of :class:`warnings.WarningMessage` objects, + one for each warning raised. + + This function can be used as a context manager, or any of the other ways :func:`pytest.raises` can be used:: - + >>> import pytest >>> with pytest.warns(RuntimeWarning): - ... warnings.warn("my warning", RuntimeWarning) - - In the context manager form you may use the keyword argument ``match`` to assert + ... warnings.warn("my warning", RuntimeWarning) + + In the context manager form you may use the keyword argument ``match`` to assert that the warning matches a text or regex:: - + >>> with pytest.warns(UserWarning, match='must be 0 or None'): - ... warnings.warn("value must be 0 or None", UserWarning) - + ... warnings.warn("value must be 0 or None", UserWarning) + >>> with pytest.warns(UserWarning, match=r'must be \d+$'): - ... warnings.warn("value must be 42", UserWarning) - + ... warnings.warn("value must be 42", UserWarning) + >>> with pytest.warns(UserWarning, match=r'must be \d+$'): - ... warnings.warn("this is not here", UserWarning) - Traceback (most recent call last): - ... - Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... - - """ - __tracebackhide__ = True - if not args: + ... warnings.warn("this is not here", UserWarning) + Traceback (most recent call last): + ... + Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted... + + """ + __tracebackhide__ = True + if not args: if kwargs: msg = "Unexpected keyword arguments passed to pytest.warns: " msg += ", ".join(sorted(kwargs)) msg += "\nUse context-manager form instead?" raise TypeError(msg) return WarningsChecker(expected_warning, match_expr=match, _ispytest=True) - else: - func = args[0] + else: + func = args[0] if not callable(func): raise TypeError( "{!r} object (type: {}) must be callable".format(func, type(func)) ) with WarningsChecker(expected_warning, _ispytest=True): - return func(*args[1:], **kwargs) - - -class WarningsRecorder(warnings.catch_warnings): - """A context manager to record raised warnings. - - Adapted from `warnings.catch_warnings`. - """ - + return func(*args[1:], **kwargs) + + +class WarningsRecorder(warnings.catch_warnings): + """A context manager to record raised warnings. + + Adapted from `warnings.catch_warnings`. + """ + def __init__(self, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) # Type ignored due to the way typeshed handles warnings.catch_warnings. super().__init__(record=True) # type: ignore[call-arg] - self._entered = False + self._entered = False self._list: List[warnings.WarningMessage] = [] - - @property + + @property def list(self) -> List["warnings.WarningMessage"]: - """The list of recorded warnings.""" - return self._list - + """The list of recorded warnings.""" + return self._list + def __getitem__(self, i: int) -> "warnings.WarningMessage": - """Get a recorded warning by index.""" - return self._list[i] - + """Get a recorded warning by index.""" + return self._list[i] + def __iter__(self) -> Iterator["warnings.WarningMessage"]: - """Iterate through the recorded warnings.""" - return iter(self._list) - + """Iterate through the recorded warnings.""" + return iter(self._list) + def __len__(self) -> int: - """The number of recorded warnings.""" - return len(self._list) - + """The number of recorded warnings.""" + return len(self._list) + def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": - """Pop the first recorded warning, raise exception if not exists.""" - for i, w in enumerate(self._list): - if issubclass(w.category, cls): - return self._list.pop(i) - __tracebackhide__ = True - raise AssertionError("%r not found in warning list" % cls) - + """Pop the first recorded warning, raise exception if not exists.""" + for i, w in enumerate(self._list): + if issubclass(w.category, cls): + return self._list.pop(i) + __tracebackhide__ = True + raise AssertionError("%r not found in warning list" % cls) + def clear(self) -> None: - """Clear the list of recorded warnings.""" - self._list[:] = [] - + """Clear the list of recorded warnings.""" + self._list[:] = [] + # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__ # -- it returns a List but we only emulate one. def __enter__(self) -> "WarningsRecorder": # type: ignore - if self._entered: - __tracebackhide__ = True - raise RuntimeError("Cannot enter %r twice" % self) + if self._entered: + __tracebackhide__ = True + raise RuntimeError("Cannot enter %r twice" % self) _list = super().__enter__() # record=True means it's None. assert _list is not None self._list = _list - warnings.simplefilter("always") - return self - + warnings.simplefilter("always") + return self + def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> None: - if not self._entered: - __tracebackhide__ = True - raise RuntimeError("Cannot exit %r without entering first" % self) - + if not self._entered: + __tracebackhide__ = True + raise RuntimeError("Cannot exit %r without entering first" % self) + super().__exit__(exc_type, exc_val, exc_tb) # Built-in catch_warnings does not reset entered state so we do it # manually here for this context manager to become reusable. self._entered = False - + @final -class WarningsChecker(WarningsRecorder): +class WarningsChecker(WarningsRecorder): def __init__( self, expected_warning: Optional[ @@ -241,23 +241,23 @@ class WarningsChecker(WarningsRecorder): ) -> None: check_ispytest(_ispytest) super().__init__(_ispytest=True) - + msg = "exceptions must be derived from Warning, not %s" if expected_warning is None: expected_warning_tup = None elif isinstance(expected_warning, tuple): - for exc in expected_warning: + for exc in expected_warning: if not issubclass(exc, Warning): - raise TypeError(msg % type(exc)) + raise TypeError(msg % type(exc)) expected_warning_tup = expected_warning elif issubclass(expected_warning, Warning): expected_warning_tup = (expected_warning,) else: - raise TypeError(msg % type(expected_warning)) - + raise TypeError(msg % type(expected_warning)) + self.expected_warning = expected_warning_tup - self.match_expr = match_expr - + self.match_expr = match_expr + def __exit__( self, exc_type: Optional[Type[BaseException]], @@ -265,32 +265,32 @@ class WarningsChecker(WarningsRecorder): exc_tb: Optional[TracebackType], ) -> None: super().__exit__(exc_type, exc_val, exc_tb) - - __tracebackhide__ = True - - # only check if we're not currently handling an exception + + __tracebackhide__ = True + + # only check if we're not currently handling an exception if exc_type is None and exc_val is None and exc_tb is None: - if self.expected_warning is not None: - if not any(issubclass(r.category, self.expected_warning) for r in self): - __tracebackhide__ = True - fail( - "DID NOT WARN. No warnings of type {} was emitted. " - "The list of emitted warnings is: {}.".format( - self.expected_warning, [each.message for each in self] - ) - ) - elif self.match_expr is not None: - for r in self: - if issubclass(r.category, self.expected_warning): - if re.compile(self.match_expr).search(str(r.message)): - break - else: - fail( - "DID NOT WARN. No warnings of type {} matching" - " ('{}') was emitted. The list of emitted warnings" - " is: {}.".format( - self.expected_warning, - self.match_expr, - [each.message for each in self], - ) - ) + if self.expected_warning is not None: + if not any(issubclass(r.category, self.expected_warning) for r in self): + __tracebackhide__ = True + fail( + "DID NOT WARN. No warnings of type {} was emitted. " + "The list of emitted warnings is: {}.".format( + self.expected_warning, [each.message for each in self] + ) + ) + elif self.match_expr is not None: + for r in self: + if issubclass(r.category, self.expected_warning): + if re.compile(self.match_expr).search(str(r.message)): + break + else: + fail( + "DID NOT WARN. No warnings of type {} matching" + " ('{}') was emitted. The list of emitted warnings" + " is: {}.".format( + self.expected_warning, + self.match_expr, + [each.message for each in self], + ) + ) diff --git a/contrib/python/pytest/py3/_pytest/reports.py b/contrib/python/pytest/py3/_pytest/reports.py index 7c0c5b60d6..58f12517c5 100644 --- a/contrib/python/pytest/py3/_pytest/reports.py +++ b/contrib/python/pytest/py3/_pytest/reports.py @@ -15,8 +15,8 @@ from typing import TypeVar from typing import Union import attr -import py - +import py + from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionRepr @@ -27,33 +27,33 @@ from _pytest._code.code import ReprFileLocation from _pytest._code.code import ReprFuncArgs from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback -from _pytest._code.code import TerminalRepr +from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter from _pytest.compat import final from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import skip - + if TYPE_CHECKING: from typing import NoReturn from typing_extensions import Literal - + from _pytest.runner import CallInfo def getworkerinfoline(node): - try: + try: return node._workerinfocache - except AttributeError: + except AttributeError: d = node.workerinfo - ver = "%s.%s.%s" % d["version_info"][:3] + ver = "%s.%s.%s" % d["version_info"][:3] node._workerinfocache = s = "[{}] {} -- Python {} {}".format( d["id"], d["sysplatform"], ver, d["executable"] - ) - return s - - + ) + return s + + _R = TypeVar("_R", bound="BaseReport") @@ -67,93 +67,93 @@ class BaseReport: nodeid: str def __init__(self, **kw: Any) -> None: - self.__dict__.update(kw) - + self.__dict__.update(kw) + if TYPE_CHECKING: # Can have arbitrary fields given to __init__(). def __getattr__(self, key: str) -> Any: ... def toterminal(self, out: TerminalWriter) -> None: - if hasattr(self, "node"): + if hasattr(self, "node"): out.line(getworkerinfoline(self.node)) - - longrepr = self.longrepr - if longrepr is None: - return - - if hasattr(longrepr, "toterminal"): + + longrepr = self.longrepr + if longrepr is None: + return + + if hasattr(longrepr, "toterminal"): longrepr_terminal = cast(TerminalRepr, longrepr) longrepr_terminal.toterminal(out) - else: - try: + else: + try: s = str(longrepr) - except UnicodeEncodeError: + except UnicodeEncodeError: s = "<unprintable longrepr>" out.line(s) - + def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]: - for name, content in self.sections: - if name.startswith(prefix): - yield prefix, content - - @property + for name, content in self.sections: + if name.startswith(prefix): + yield prefix, content + + @property def longreprtext(self) -> str: """Read-only property that returns the full string representation of ``longrepr``. - - .. versionadded:: 3.0 - """ + + .. versionadded:: 3.0 + """ file = StringIO() tw = TerminalWriter(file) - tw.hasmarkup = False - self.toterminal(tw) + tw.hasmarkup = False + self.toterminal(tw) exc = file.getvalue() - return exc.strip() - - @property + return exc.strip() + + @property def caplog(self) -> str: """Return captured log lines, if log capturing is enabled. - - .. versionadded:: 3.5 - """ - return "\n".join( - content for (prefix, content) in self.get_sections("Captured log") - ) - - @property + + .. versionadded:: 3.5 + """ + return "\n".join( + content for (prefix, content) in self.get_sections("Captured log") + ) + + @property def capstdout(self) -> str: """Return captured text from stdout, if capturing is enabled. - - .. versionadded:: 3.0 - """ - return "".join( - content for (prefix, content) in self.get_sections("Captured stdout") - ) - - @property + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stdout") + ) + + @property def capstderr(self) -> str: """Return captured text from stderr, if capturing is enabled. - - .. versionadded:: 3.0 - """ - return "".join( - content for (prefix, content) in self.get_sections("Captured stderr") - ) - - passed = property(lambda x: x.outcome == "passed") - failed = property(lambda x: x.outcome == "failed") - skipped = property(lambda x: x.outcome == "skipped") - - @property + + .. versionadded:: 3.0 + """ + return "".join( + content for (prefix, content) in self.get_sections("Captured stderr") + ) + + passed = property(lambda x: x.outcome == "passed") + failed = property(lambda x: x.outcome == "failed") + skipped = property(lambda x: x.outcome == "skipped") + + @property def fspath(self) -> str: - return self.nodeid.split("::")[0] - + return self.nodeid.split("::")[0] + @property def count_towards_summary(self) -> bool: """**Experimental** Whether this report should be counted towards the totals shown at the end of the test session: "1 passed, 1 failure, etc". - + .. note:: This function is considered **experimental**, so beware that it is subject to changes @@ -227,17 +227,17 @@ def _report_unserialization_failure( @final -class TestReport(BaseReport): +class TestReport(BaseReport): """Basic test report object (also used for setup and teardown calls if they fail).""" - + __test__ = False - def __init__( - self, + def __init__( + self, nodeid: str, location: Tuple[str, Optional[int], str], - keywords, + keywords, outcome: "Literal['passed', 'failed', 'skipped']", longrepr: Union[ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr @@ -249,46 +249,46 @@ class TestReport(BaseReport): **extra, ) -> None: #: Normalized collection nodeid. - self.nodeid = nodeid - + self.nodeid = nodeid + #: A (filesystempath, lineno, domaininfo) tuple indicating 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. + #: 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. self.location: Tuple[str, Optional[int], str] = location - + #: A name -> value dictionary containing all keywords and - #: markers associated with a test invocation. - self.keywords = keywords - + #: markers associated with a test invocation. + self.keywords = keywords + #: Test outcome, always one of "passed", "failed", "skipped". - self.outcome = outcome - - #: None or a failure representation. - self.longrepr = longrepr - + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + #: 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 #: 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 - #: marshallable. Used by pytest to add captured text - #: from ``stdout`` and ``stderr``, but may be used by other plugins - #: to add arbitrary information to reports. - self.sections = list(sections) - + #: marshallable. Used by pytest to add captured text + #: from ``stdout`` and ``stderr``, but may be used by other plugins + #: to add arbitrary information to reports. + self.sections = list(sections) + #: Time it took to run just the test. - self.duration = duration - - self.__dict__.update(extra) - + self.duration = duration + + self.__dict__.update(extra) + def __repr__(self) -> str: return "<{} {!r} when={!r} outcome={!r}>".format( self.__class__.__name__, self.nodeid, self.when, self.outcome - ) - + ) + @classmethod def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": """Create and fill a TestReport with standard item and call info.""" @@ -337,14 +337,14 @@ class TestReport(BaseReport): duration, user_properties=item.user_properties, ) - - + + @final class CollectReport(BaseReport): """Collection report object.""" when = "collect" - + def __init__( self, nodeid: str, @@ -355,42 +355,42 @@ class CollectReport(BaseReport): **extra, ) -> None: #: Normalized collection nodeid. - self.nodeid = nodeid + self.nodeid = nodeid #: Test outcome, always one of "passed", "failed", "skipped". - self.outcome = outcome + self.outcome = outcome #: None or a failure representation. - self.longrepr = longrepr + self.longrepr = longrepr #: 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 #: marshallable. # Used by pytest to add captured text : from ``stdout`` and ``stderr``, # but may be used by other plugins : to add arbitrary information to # reports. - self.sections = list(sections) - - self.__dict__.update(extra) - - @property - def location(self): - return (self.fspath, None, self.fspath) - + self.sections = list(sections) + + self.__dict__.update(extra) + + @property + def location(self): + return (self.fspath, None, self.fspath) + def __repr__(self) -> str: return "<CollectReport {!r} lenresult={} outcome={!r}>".format( self.nodeid, len(self.result), self.outcome - ) - - -class CollectErrorRepr(TerminalRepr): + ) + + +class CollectErrorRepr(TerminalRepr): def __init__(self, msg: str) -> None: - self.longrepr = msg - + self.longrepr = msg + def toterminal(self, out: TerminalWriter) -> None: - out.line(self.longrepr, red=True) + out.line(self.longrepr, red=True) def pytest_report_to_serializable( diff --git a/contrib/python/pytest/py3/_pytest/runner.py b/contrib/python/pytest/py3/_pytest/runner.py index fc74e6eb0e..794690ddb0 100644 --- a/contrib/python/pytest/py3/_pytest/runner.py +++ b/contrib/python/pytest/py3/_pytest/runner.py @@ -1,7 +1,7 @@ """Basic collect and runtest protocol implementations.""" -import bdb -import os -import sys +import bdb +import os +import sys from typing import Callable from typing import cast from typing import Dict @@ -13,16 +13,16 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union - + import attr - + from .reports import BaseReport -from .reports import CollectErrorRepr -from .reports import CollectReport -from .reports import TestReport +from .reports import CollectErrorRepr +from .reports import CollectReport +from .reports import TestReport from _pytest import timing from _pytest._code.code import ExceptionChainRepr -from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest.compat import final from _pytest.config.argparsing import Parser @@ -30,28 +30,28 @@ from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.nodes import Node from _pytest.outcomes import Exit -from _pytest.outcomes import Skipped -from _pytest.outcomes import TEST_OUTCOME - +from _pytest.outcomes import Skipped +from _pytest.outcomes import TEST_OUTCOME + if TYPE_CHECKING: from typing_extensions import Literal from _pytest.main import Session from _pytest.terminal import TerminalReporter -# +# # pytest plugin hooks. - - + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("terminal reporting", "reporting", after="general") - group.addoption( - "--durations", - action="store", - type=int, - default=None, - metavar="N", - help="show N slowest setup/test durations (N=0 for all).", + group = parser.getgroup("terminal reporting", "reporting", after="general") + group.addoption( + "--durations", + action="store", + type=int, + default=None, + metavar="N", + help="show N slowest setup/test durations (N=0 for all).", ) group.addoption( "--durations-min", @@ -61,167 +61,167 @@ def pytest_addoption(parser: Parser) -> None: metavar="N", help="Minimal duration in seconds for inclusion in slowest list. Default 0.005", ) - - + + def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: - durations = terminalreporter.config.option.durations + durations = terminalreporter.config.option.durations durations_min = terminalreporter.config.option.durations_min - verbose = terminalreporter.config.getvalue("verbose") - if durations is None: - return - tr = terminalreporter - dlist = [] - for replist in tr.stats.values(): - for rep in replist: - if hasattr(rep, "duration"): - dlist.append(rep) - if not dlist: - return + verbose = terminalreporter.config.getvalue("verbose") + if durations is None: + return + tr = terminalreporter + dlist = [] + for replist in tr.stats.values(): + for rep in replist: + if hasattr(rep, "duration"): + dlist.append(rep) + if not dlist: + return dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return] - if not durations: + if not durations: tr.write_sep("=", "slowest durations") - else: + else: tr.write_sep("=", "slowest %s durations" % durations) - dlist = dlist[:durations] - + dlist = dlist[:durations] + for i, rep in enumerate(dlist): if verbose < 2 and rep.duration < durations_min: - tr.write_line("") + tr.write_line("") tr.write_line( "(%s durations < %gs hidden. Use -vv to show these durations.)" % (len(dlist) - i, durations_min) ) - break + break tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}") - - + + def pytest_sessionstart(session: "Session") -> None: - session._setupstate = SetupState() - - + session._setupstate = SetupState() + + def pytest_sessionfinish(session: "Session") -> None: - session._setupstate.teardown_all() - - + session._setupstate.teardown_all() + + def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: ihook = item.ihook ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) - runtestprotocol(item, nextitem=nextitem) + runtestprotocol(item, nextitem=nextitem) ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) - return True - - + return True + + def runtestprotocol( item: Item, log: bool = True, nextitem: Optional[Item] = None ) -> List[TestReport]: - hasrequest = hasattr(item, "_request") + hasrequest = hasattr(item, "_request") if hasrequest and not item._request: # type: ignore[attr-defined] item._initrequest() # type: ignore[attr-defined] - rep = call_and_report(item, "setup", log) - reports = [rep] - if rep.passed: + rep = call_and_report(item, "setup", log) + reports = [rep] + if rep.passed: if item.config.getoption("setupshow", False): - show_test_item(item) + show_test_item(item) if not item.config.getoption("setuponly", False): - reports.append(call_and_report(item, "call", log)) - reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) + reports.append(call_and_report(item, "call", log)) + reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) # After all teardown hooks have been called # want funcargs and request info to go away. - if hasrequest: + if hasrequest: item._request = False # type: ignore[attr-defined] item.funcargs = None # type: ignore[attr-defined] - return reports - - + return reports + + def show_test_item(item: Item) -> None: - """Show test function, parameters and the fixtures of the test item.""" - tw = item.config.get_terminal_writer() - tw.line() - tw.write(" " * 8) + """Show test function, parameters and the fixtures of the test item.""" + tw = item.config.get_terminal_writer() + tw.line() + tw.write(" " * 8) tw.write(item.nodeid) used_fixtures = sorted(getattr(item, "fixturenames", [])) - if used_fixtures: - tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) + if used_fixtures: + tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) tw.flush() - - + + def pytest_runtest_setup(item: Item) -> None: - _update_current_test_var(item, "setup") - item.session._setupstate.prepare(item) - - + _update_current_test_var(item, "setup") + item.session._setupstate.prepare(item) + + def pytest_runtest_call(item: Item) -> None: - _update_current_test_var(item, "call") - try: + _update_current_test_var(item, "call") + try: del sys.last_type del sys.last_value del sys.last_traceback except AttributeError: pass try: - item.runtest() + item.runtest() except Exception as e: - # Store trace info to allow postmortem debugging + # Store trace info to allow postmortem debugging sys.last_type = type(e) sys.last_value = e assert e.__traceback__ is not None # Skip *this* frame sys.last_traceback = e.__traceback__.tb_next raise e - - + + def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: - _update_current_test_var(item, "teardown") - item.session._setupstate.teardown_exact(item, nextitem) - _update_current_test_var(item, None) - - + _update_current_test_var(item, "teardown") + item.session._setupstate.teardown_exact(item, nextitem) + _update_current_test_var(item, None) + + def _update_current_test_var( item: Item, when: Optional["Literal['setup', 'call', 'teardown']"] ) -> None: """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. - + If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment. - """ - var_name = "PYTEST_CURRENT_TEST" - if when: + """ + var_name = "PYTEST_CURRENT_TEST" + if when: value = f"{item.nodeid} ({when})" - # don't allow null bytes on environment variables (see #2644, #2957) - value = value.replace("\x00", "(null)") - os.environ[var_name] = value - else: - os.environ.pop(var_name) - - + # don't allow null bytes on environment variables (see #2644, #2957) + value = value.replace("\x00", "(null)") + os.environ[var_name] = value + else: + os.environ.pop(var_name) + + def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: - if report.when in ("setup", "teardown"): - if report.failed: - # category, shortletter, verbose-word - return "error", "E", "ERROR" - elif report.skipped: - return "skipped", "s", "SKIPPED" - else: - return "", "", "" + if report.when in ("setup", "teardown"): + if report.failed: + # category, shortletter, verbose-word + return "error", "E", "ERROR" + elif report.skipped: + return "skipped", "s", "SKIPPED" + else: + return "", "", "" return None - - -# -# Implementation - - + + +# +# Implementation + + def call_and_report( item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds ) -> TestReport: - call = call_runtest_hook(item, when, **kwds) - hook = item.ihook + call = call_runtest_hook(item, when, **kwds) + hook = item.ihook report: TestReport = hook.pytest_runtest_makereport(item=item, call=call) - if log: - hook.pytest_runtest_logreport(report=report) - if check_interactive_exception(call, report): - hook.pytest_exception_interact(node=item, call=call, report=report) - return report - - + if log: + hook.pytest_runtest_logreport(report=report) + if check_interactive_exception(call, report): + hook.pytest_exception_interact(node=item, call=call, report=report) + return report + + def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool: """Check whether the call raised an exception that should be reported as interactive.""" @@ -235,8 +235,8 @@ def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> # Special control flow exception. return False return True - - + + def call_runtest_hook( item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds ) -> "CallInfo[None]": @@ -253,9 +253,9 @@ def call_runtest_hook( reraise += (KeyboardInterrupt,) return CallInfo.from_call( lambda: ihook(item=item, **kwds), when=when, reraise=reraise - ) - - + ) + + TResult = TypeVar("TResult", covariant=True) @@ -263,7 +263,7 @@ TResult = TypeVar("TResult", covariant=True) @attr.s(repr=False) class CallInfo(Generic[TResult]): """Result/Exception info a function invocation. - + :param T result: The return value of the call, if it didn't raise. Can only be accessed if excinfo is None. @@ -278,7 +278,7 @@ class CallInfo(Generic[TResult]): :param str when: The context of invocation: "setup", "call", "teardown", ... """ - + _result = attr.ib(type="Optional[TResult]") excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]]) start = attr.ib(type=float) @@ -307,12 +307,12 @@ class CallInfo(Generic[TResult]): excinfo = None start = timing.time() precise_start = timing.perf_counter() - try: + try: result: Optional[TResult] = func() except BaseException: excinfo = ExceptionInfo.from_current() if reraise is not None and isinstance(excinfo.value, reraise): - raise + raise result = None # use the perf counter precise_stop = timing.perf_counter() @@ -326,137 +326,137 @@ class CallInfo(Generic[TResult]): result=result, excinfo=excinfo, ) - + def __repr__(self) -> str: if self.excinfo is None: return f"<CallInfo when={self.when!r} result: {self._result!r}>" return f"<CallInfo when={self.when!r} excinfo={self.excinfo!r}>" - - + + def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: return TestReport.from_item_and_call(item, call) - - + + def pytest_make_collect_report(collector: Collector) -> CollectReport: call = CallInfo.from_call(lambda: list(collector.collect()), "collect") longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None - if not call.excinfo: + if not call.excinfo: outcome: Literal["passed", "skipped", "failed"] = "passed" - else: + else: skip_exceptions = [Skipped] unittest = sys.modules.get("unittest") if unittest is not None: # Type ignored because unittest is loaded dynamically. skip_exceptions.append(unittest.SkipTest) # type: ignore if isinstance(call.excinfo.value, tuple(skip_exceptions)): - outcome = "skipped" + outcome = "skipped" r_ = collector._repr_failure_py(call.excinfo, "line") assert isinstance(r_, ExceptionChainRepr), repr(r_) r = r_.reprcrash assert r - longrepr = (str(r.path), r.lineno, r.message) - else: - outcome = "failed" - errorinfo = collector.repr_failure(call.excinfo) - if not hasattr(errorinfo, "toterminal"): + longrepr = (str(r.path), r.lineno, r.message) + else: + outcome = "failed" + errorinfo = collector.repr_failure(call.excinfo) + if not hasattr(errorinfo, "toterminal"): assert isinstance(errorinfo, str) - errorinfo = CollectErrorRepr(errorinfo) - longrepr = errorinfo + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo result = call.result if not call.excinfo else None rep = CollectReport(collector.nodeid, outcome, longrepr, result) rep.call = call # type: ignore # see collect_one_node - return rep - - + return rep + + class SetupState: """Shared state for setting up/tearing down test items or collectors.""" - - def __init__(self): + + def __init__(self): self.stack: List[Node] = [] self._finalizers: Dict[Node, List[Callable[[], object]]] = {} - + def addfinalizer(self, finalizer: Callable[[], object], colitem) -> None: """Attach a finalizer to the given colitem.""" - assert colitem and not isinstance(colitem, tuple) - assert callable(finalizer) - # assert colitem in self.stack # some unit tests don't setup stack :/ - self._finalizers.setdefault(colitem, []).append(finalizer) - - def _pop_and_teardown(self): - colitem = self.stack.pop() - self._teardown_with_finalization(colitem) - + assert colitem and not isinstance(colitem, tuple) + assert callable(finalizer) + # assert colitem in self.stack # some unit tests don't setup stack :/ + self._finalizers.setdefault(colitem, []).append(finalizer) + + def _pop_and_teardown(self): + colitem = self.stack.pop() + self._teardown_with_finalization(colitem) + def _callfinalizers(self, colitem) -> None: - finalizers = self._finalizers.pop(colitem, None) - exc = None - while finalizers: - fin = finalizers.pop() - try: - fin() + finalizers = self._finalizers.pop(colitem, None) + exc = None + while finalizers: + fin = finalizers.pop() + try: + fin() except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if exc is None: exc = e - if exc: + if exc: raise exc - + def _teardown_with_finalization(self, colitem) -> None: - self._callfinalizers(colitem) + self._callfinalizers(colitem) colitem.teardown() - for colitem in self._finalizers: + for colitem in self._finalizers: assert colitem in self.stack - + def teardown_all(self) -> None: - while self.stack: - self._pop_and_teardown() - for key in list(self._finalizers): - self._teardown_with_finalization(key) - assert not self._finalizers - + while self.stack: + self._pop_and_teardown() + for key in list(self._finalizers): + self._teardown_with_finalization(key) + assert not self._finalizers + def teardown_exact(self, item, nextitem) -> None: - needed_collectors = nextitem and nextitem.listchain() or [] - self._teardown_towards(needed_collectors) - + needed_collectors = nextitem and nextitem.listchain() or [] + self._teardown_towards(needed_collectors) + def _teardown_towards(self, needed_collectors) -> None: - exc = None - while self.stack: - if self.stack == needed_collectors[: len(self.stack)]: - break - try: - self._pop_and_teardown() + exc = None + while self.stack: + if self.stack == needed_collectors[: len(self.stack)]: + break + try: + self._pop_and_teardown() except TEST_OUTCOME as e: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if exc is None: exc = e - if exc: + if exc: raise exc - + def prepare(self, colitem) -> None: """Setup objects along the collector chain to the test-method.""" - + # Check if the last collection node has raised an error. - for col in self.stack: - if hasattr(col, "_prepare_exc"): + for col in self.stack: + if hasattr(col, "_prepare_exc"): exc = col._prepare_exc # type: ignore[attr-defined] raise exc needed_collectors = colitem.listchain() - for col in needed_collectors[len(self.stack) :]: - self.stack.append(col) - try: - col.setup() + for col in needed_collectors[len(self.stack) :]: + self.stack.append(col) + try: + col.setup() except TEST_OUTCOME as e: col._prepare_exc = e # type: ignore[attr-defined] raise e - - + + def collect_one_node(collector: Collector) -> CollectReport: - ihook = collector.ihook - ihook.pytest_collectstart(collector=collector) + ihook = collector.ihook + ihook.pytest_collectstart(collector=collector) rep: CollectReport = ihook.pytest_make_collect_report(collector=collector) - call = rep.__dict__.pop("call", None) - if call and check_interactive_exception(call, rep): - ihook.pytest_exception_interact(node=collector, call=call, report=rep) - return rep + call = rep.__dict__.pop("call", None) + if call and check_interactive_exception(call, rep): + ihook.pytest_exception_interact(node=collector, call=call, report=rep) + return rep diff --git a/contrib/python/pytest/py3/_pytest/setuponly.py b/contrib/python/pytest/py3/_pytest/setuponly.py index 8a284d3d51..44a1094c0d 100644 --- a/contrib/python/pytest/py3/_pytest/setuponly.py +++ b/contrib/python/pytest/py3/_pytest/setuponly.py @@ -2,93 +2,93 @@ from typing import Generator from typing import Optional from typing import Union -import pytest +import pytest from _pytest._io.saferepr import saferepr from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureDef from _pytest.fixtures import SubRequest - - + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("debugconfig") - group.addoption( - "--setuponly", - "--setup-only", - action="store_true", - help="only setup fixtures, do not execute tests.", - ) - group.addoption( - "--setupshow", - "--setup-show", - action="store_true", - help="show setup of fixtures while executing tests.", - ) - - -@pytest.hookimpl(hookwrapper=True) + group = parser.getgroup("debugconfig") + group.addoption( + "--setuponly", + "--setup-only", + action="store_true", + help="only setup fixtures, do not execute tests.", + ) + group.addoption( + "--setupshow", + "--setup-show", + action="store_true", + help="show setup of fixtures while executing tests.", + ) + + +@pytest.hookimpl(hookwrapper=True) def pytest_fixture_setup( fixturedef: FixtureDef[object], request: SubRequest ) -> Generator[None, None, None]: - yield + yield if request.config.option.setupshow: - if hasattr(request, "param"): - # Save the fixture parameter so ._show_fixture_action() can - # display it now and during the teardown (in .finish()). - if fixturedef.ids: - if callable(fixturedef.ids): + if hasattr(request, "param"): + # Save the fixture parameter so ._show_fixture_action() can + # display it now and during the teardown (in .finish()). + if fixturedef.ids: + if callable(fixturedef.ids): param = fixturedef.ids(request.param) - else: + else: param = fixturedef.ids[request.param_index] - else: + else: param = request.param fixturedef.cached_param = param # type: ignore[attr-defined] - _show_fixture_action(fixturedef, "SETUP") - - + _show_fixture_action(fixturedef, "SETUP") + + def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None: if fixturedef.cached_result is not None: - config = fixturedef._fixturemanager.config - if config.option.setupshow: - _show_fixture_action(fixturedef, "TEARDOWN") - if hasattr(fixturedef, "cached_param"): + config = fixturedef._fixturemanager.config + if config.option.setupshow: + _show_fixture_action(fixturedef, "TEARDOWN") + if hasattr(fixturedef, "cached_param"): del fixturedef.cached_param # type: ignore[attr-defined] - - + + def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None: - config = fixturedef._fixturemanager.config - capman = config.pluginmanager.getplugin("capturemanager") - if capman: - capman.suspend_global_capture() - - tw = config.get_terminal_writer() - tw.line() - tw.write(" " * 2 * fixturedef.scopenum) - tw.write( - "{step} {scope} {fixture}".format( - step=msg.ljust(8), # align the output to TEARDOWN - scope=fixturedef.scope[0].upper(), - fixture=fixturedef.argname, - ) - ) - - if msg == "SETUP": - deps = sorted(arg for arg in fixturedef.argnames if arg != "request") - if deps: - tw.write(" (fixtures used: {})".format(", ".join(deps))) - - if hasattr(fixturedef, "cached_param"): + config = fixturedef._fixturemanager.config + capman = config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture() + + tw = config.get_terminal_writer() + tw.line() + tw.write(" " * 2 * fixturedef.scopenum) + tw.write( + "{step} {scope} {fixture}".format( + step=msg.ljust(8), # align the output to TEARDOWN + scope=fixturedef.scope[0].upper(), + fixture=fixturedef.argname, + ) + ) + + if msg == "SETUP": + deps = sorted(arg for arg in fixturedef.argnames if arg != "request") + if deps: + tw.write(" (fixtures used: {})".format(", ".join(deps))) + + if hasattr(fixturedef, "cached_param"): tw.write("[{}]".format(saferepr(fixturedef.cached_param, maxsize=42))) # type: ignore[attr-defined] - + tw.flush() - if capman: - capman.resume_global_capture() - - -@pytest.hookimpl(tryfirst=True) + if capman: + capman.resume_global_capture() + + +@pytest.hookimpl(tryfirst=True) def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: - if config.option.setuponly: - config.option.setupshow = True + if config.option.setuponly: + config.option.setupshow = True return None diff --git a/contrib/python/pytest/py3/_pytest/setupplan.py b/contrib/python/pytest/py3/_pytest/setupplan.py index 09d90763e7..9ba81ccaf0 100644 --- a/contrib/python/pytest/py3/_pytest/setupplan.py +++ b/contrib/python/pytest/py3/_pytest/setupplan.py @@ -1,40 +1,40 @@ from typing import Optional from typing import Union -import pytest +import pytest from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureDef from _pytest.fixtures import SubRequest - - + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("debugconfig") - group.addoption( - "--setupplan", - "--setup-plan", - action="store_true", - help="show what fixtures and tests would be executed but " - "don't execute anything.", - ) - - -@pytest.hookimpl(tryfirst=True) + group = parser.getgroup("debugconfig") + group.addoption( + "--setupplan", + "--setup-plan", + action="store_true", + help="show what fixtures and tests would be executed but " + "don't execute anything.", + ) + + +@pytest.hookimpl(tryfirst=True) def pytest_fixture_setup( fixturedef: FixtureDef[object], request: SubRequest ) -> Optional[object]: - # Will return a dummy fixture if the setuponly option is provided. - if request.config.option.setupplan: + # Will return a dummy fixture if the setuponly option is provided. + if request.config.option.setupplan: my_cache_key = fixturedef.cache_key(request) fixturedef.cached_result = (None, my_cache_key, None) - return fixturedef.cached_result + return fixturedef.cached_result return None - - -@pytest.hookimpl(tryfirst=True) + + +@pytest.hookimpl(tryfirst=True) def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: - if config.option.setupplan: - config.option.setuponly = True - config.option.setupshow = True + if config.option.setupplan: + config.option.setuponly = True + config.option.setupshow = True return None diff --git a/contrib/python/pytest/py3/_pytest/skipping.py b/contrib/python/pytest/py3/_pytest/skipping.py index 791b0baf44..9aacfecee7 100644 --- a/contrib/python/pytest/py3/_pytest/skipping.py +++ b/contrib/python/pytest/py3/_pytest/skipping.py @@ -12,79 +12,79 @@ from typing import Type import attr from _pytest.config import Config -from _pytest.config import hookimpl +from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.mark.structures import Mark from _pytest.nodes import Item -from _pytest.outcomes import fail -from _pytest.outcomes import skip -from _pytest.outcomes import xfail +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import xfail from _pytest.reports import BaseReport from _pytest.runner import CallInfo from _pytest.store import StoreKey - - + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group.addoption( - "--runxfail", - action="store_true", - dest="runxfail", - default=False, + group = parser.getgroup("general") + group.addoption( + "--runxfail", + action="store_true", + dest="runxfail", + default=False, help="report the results of xfail tests as if they were not marked", - ) - - parser.addini( - "xfail_strict", - "default for the strict parameter of xfail " - "markers when not given explicitly (default: False)", - default=False, - type="bool", - ) - - + ) + + parser.addini( + "xfail_strict", + "default for the strict parameter of xfail " + "markers when not given explicitly (default: False)", + default=False, + type="bool", + ) + + def pytest_configure(config: Config) -> None: - if config.option.runxfail: - # yay a hack - import pytest - - old = pytest.xfail - config._cleanup.append(lambda: setattr(pytest, "xfail", old)) - - def nop(*args, **kwargs): - pass - + if config.option.runxfail: + # yay a hack + import pytest + + old = pytest.xfail + config._cleanup.append(lambda: setattr(pytest, "xfail", old)) + + def nop(*args, **kwargs): + pass + nop.Exception = xfail.Exception # type: ignore[attr-defined] - setattr(pytest, "xfail", nop) - - config.addinivalue_line( - "markers", - "skip(reason=None): skip the given test function with an optional reason. " - 'Example: skip(reason="no way of currently testing this") skips the ' - "test.", - ) - config.addinivalue_line( - "markers", + setattr(pytest, "xfail", nop) + + config.addinivalue_line( + "markers", + "skip(reason=None): skip the given test function with an optional reason. " + 'Example: skip(reason="no way of currently testing this") skips the ' + "test.", + ) + config.addinivalue_line( + "markers", "skipif(condition, ..., *, reason=...): " "skip the given test function if any of the conditions evaluate to True. " "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. " "See https://docs.pytest.org/en/stable/reference.html#pytest-mark-skipif", - ) - config.addinivalue_line( - "markers", + ) + config.addinivalue_line( + "markers", "xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict): " "mark the test function as an expected failure if any of the conditions " "evaluate to True. Optionally specify a reason for better reporting " - "and run=False if you don't even want to execute the test function. " - "If only specific exception(s) are expected, you can list them in " - "raises, and if the test fails in other ways, it will be reported as " + "and run=False if you don't even want to execute the test function. " + "If only specific exception(s) are expected, you can list them in " + "raises, and if the test fails in other ways, it will be reported as " "a true failure. See https://docs.pytest.org/en/stable/reference.html#pytest-mark-xfail", - ) - - + ) + + def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, str]: """Evaluate a single skipif/xfail condition. - + If an old-style string condition is given, it is eval()'d, otherwise the condition is bool()'d. If this fails, an appropriately formatted pytest.fail is raised. @@ -146,24 +146,24 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, if reason is None: if isinstance(condition, str): reason = "condition: " + condition - else: + else: # XXX better be checked at collection time msg = ( "Error evaluating %r: " % mark.name + "you need to specify reason=STRING when using booleans as conditions." ) fail(msg, pytrace=False) - + return result, reason - - + + @attr.s(slots=True, frozen=True) class Skip: """The result of evaluate_skip_marks().""" - + reason = attr.ib(type=str) - - + + def evaluate_skip_marks(item: Item) -> Optional[Skip]: """Evaluate skip and skipif marks on item, returning Skip if triggered.""" for mark in item.iter_markers(name="skipif"): @@ -171,18 +171,18 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]: conditions = mark.args else: conditions = (mark.kwargs["condition"],) - + # Unconditional. if not conditions: reason = mark.kwargs.get("reason", "") return Skip(reason) - + # If any of the conditions are true. for condition in conditions: result, reason = evaluate_condition(item, mark, condition) if result: return Skip(reason) - + for mark in item.iter_markers(name="skip"): if "reason" in mark.kwargs: reason = mark.kwargs["reason"] @@ -249,7 +249,7 @@ def pytest_runtest_setup(item: Item) -> None: xfail("[NOTRUN] " + xfailed.reason) -@hookimpl(hookwrapper=True) +@hookimpl(hookwrapper=True) def pytest_runtest_call(item: Item) -> Generator[None, None, None]: xfailed = item._store.get(xfailed_key, None) if xfailed is None: @@ -268,57 +268,57 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]: @hookimpl(hookwrapper=True) def pytest_runtest_makereport(item: Item, call: CallInfo[None]): - outcome = yield - rep = outcome.get_result() + outcome = yield + rep = outcome.get_result() xfailed = item._store.get(xfailed_key, None) # unittest special case, see setting of unexpectedsuccess_key if unexpectedsuccess_key in item._store and rep.when == "call": reason = item._store[unexpectedsuccess_key] if reason: rep.longrepr = f"Unexpected success: {reason}" - else: - rep.longrepr = "Unexpected success" + else: + rep.longrepr = "Unexpected success" rep.outcome = "failed" - elif item.config.option.runxfail: + elif item.config.option.runxfail: pass # don't interfere elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): assert call.excinfo.value.msg is not None - rep.wasxfail = "reason: " + call.excinfo.value.msg - rep.outcome = "skipped" + rep.wasxfail = "reason: " + call.excinfo.value.msg + rep.outcome = "skipped" elif not rep.skipped and xfailed: - if call.excinfo: + if call.excinfo: raises = xfailed.raises if raises is not None and not isinstance(call.excinfo.value, raises): - rep.outcome = "failed" - else: - rep.outcome = "skipped" + rep.outcome = "failed" + else: + rep.outcome = "skipped" rep.wasxfail = xfailed.reason - elif call.when == "call": + elif call.when == "call": if xfailed.strict: - rep.outcome = "failed" + rep.outcome = "failed" rep.longrepr = "[XPASS(strict)] " + xfailed.reason - else: - rep.outcome = "passed" + else: + rep.outcome = "passed" rep.wasxfail = xfailed.reason if ( item._store.get(skipped_by_mark_key, True) - and rep.skipped - and type(rep.longrepr) is tuple - ): + and rep.skipped + and type(rep.longrepr) is tuple + ): # 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. _, _, reason = rep.longrepr filename, line = item.reportinfo()[:2] assert line is not None rep.longrepr = str(filename), line + 1, reason - - + + def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: - if hasattr(report, "wasxfail"): - if report.skipped: + if hasattr(report, "wasxfail"): + if report.skipped: return "xfailed", "x", "XFAIL" - elif report.passed: + elif report.passed: return "xpassed", "X", "XPASS" return None diff --git a/contrib/python/pytest/py3/_pytest/stepwise.py b/contrib/python/pytest/py3/_pytest/stepwise.py index faf996da57..197577c790 100644 --- a/contrib/python/pytest/py3/_pytest/stepwise.py +++ b/contrib/python/pytest/py3/_pytest/stepwise.py @@ -2,46 +2,46 @@ from typing import List from typing import Optional from typing import TYPE_CHECKING -import pytest +import pytest from _pytest import nodes from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.main import Session from _pytest.reports import TestReport - + if TYPE_CHECKING: from _pytest.cacheprovider import Cache - + STEPWISE_CACHE_DIR = "cache/stepwise" def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("general") - group.addoption( - "--sw", - "--stepwise", - action="store_true", + group = parser.getgroup("general") + group.addoption( + "--sw", + "--stepwise", + action="store_true", default=False, - dest="stepwise", + dest="stepwise", help="exit on test failure and continue from last failing test next time", - ) - group.addoption( + ) + group.addoption( "--sw-skip", - "--stepwise-skip", - action="store_true", + "--stepwise-skip", + action="store_true", default=False, - dest="stepwise_skip", - help="ignore the first failing test but stop on the next failing test", - ) - - -@pytest.hookimpl + dest="stepwise_skip", + help="ignore the first failing test but stop on the next failing test", + ) + + +@pytest.hookimpl def pytest_configure(config: Config) -> None: # We should always have a cache as cache provider plugin uses tryfirst=True if config.getoption("stepwise"): config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") - - + + def pytest_sessionfinish(session: Session) -> None: if not session.config.getoption("stepwise"): assert session.config.cache is not None @@ -49,35 +49,35 @@ def pytest_sessionfinish(session: Session) -> None: session.config.cache.set(STEPWISE_CACHE_DIR, []) -class StepwisePlugin: +class StepwisePlugin: def __init__(self, config: Config) -> None: - self.config = config + self.config = config self.session: Optional[Session] = None self.report_status = "" assert config.cache is not None self.cache: Cache = config.cache self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None) self.skip: bool = config.getoption("stepwise_skip") - + def pytest_sessionstart(self, session: Session) -> None: - self.session = session - + self.session = session + def pytest_collection_modifyitems( self, config: Config, items: List[nodes.Item] ) -> None: if not self.lastfailed: self.report_status = "no previously failed tests, not skipping." return - + # check all item nodes until we find a match on last failed failed_index = None for index, item in enumerate(items): - if item.nodeid == self.lastfailed: + if item.nodeid == self.lastfailed: failed_index = index - break - - # If the previously failed test was not found among the test items, - # do not skip any tests. + break + + # If the previously failed test was not found among the test items, + # do not skip any tests. if failed_index is None: self.report_status = "previously failed test not found, not skipping." else: @@ -85,31 +85,31 @@ class StepwisePlugin: deselected = items[:failed_index] del items[:failed_index] config.hook.pytest_deselected(items=deselected) - + def pytest_runtest_logreport(self, report: TestReport) -> None: - if report.failed: - if self.skip: - # Remove test from the failed ones (if it exists) and unset the skip option - # to make sure the following tests will not be skipped. - if report.nodeid == self.lastfailed: - self.lastfailed = None - - self.skip = False - else: - # Mark test as the last failing and interrupt the test session. - self.lastfailed = report.nodeid + if report.failed: + if self.skip: + # Remove test from the failed ones (if it exists) and unset the skip option + # to make sure the following tests will not be skipped. + if report.nodeid == self.lastfailed: + self.lastfailed = None + + self.skip = False + else: + # Mark test as the last failing and interrupt the test session. + self.lastfailed = report.nodeid assert self.session is not None - self.session.shouldstop = ( - "Test failed, continuing from this test next run." - ) - - else: - # If the test was actually run and did pass. - if report.when == "call": - # Remove test from the failed ones, if exists. - if report.nodeid == self.lastfailed: - self.lastfailed = None - + self.session.shouldstop = ( + "Test failed, continuing from this test next run." + ) + + else: + # If the test was actually run and did pass. + if report.when == "call": + # Remove test from the failed ones, if exists. + if report.nodeid == self.lastfailed: + self.lastfailed = None + def pytest_report_collectionfinish(self) -> Optional[str]: if self.config.getoption("verbose") >= 0 and self.report_status: return f"stepwise: {self.report_status}" diff --git a/contrib/python/pytest/py3/_pytest/terminal.py b/contrib/python/pytest/py3/_pytest/terminal.py index 7e317310e1..fbfb09aecf 100644 --- a/contrib/python/pytest/py3/_pytest/terminal.py +++ b/contrib/python/pytest/py3/_pytest/terminal.py @@ -1,12 +1,12 @@ """Terminal reporting of the full testing process. - -This is a good source for looking at the various reporting hooks. -""" -import argparse + +This is a good source for looking at the various reporting hooks. +""" +import argparse import datetime import inspect -import platform -import sys +import platform +import sys import warnings from collections import Counter from functools import partial @@ -25,13 +25,13 @@ from typing import TextIO from typing import Tuple from typing import TYPE_CHECKING from typing import Union - -import attr -import pluggy -import py - + +import attr +import pluggy +import py + import _pytest._version -from _pytest import nodes +from _pytest import nodes from _pytest import timing from _pytest._code import ExceptionInfo from _pytest._code.code import ExceptionRepr @@ -49,7 +49,7 @@ from _pytest.pathlib import bestrelpath from _pytest.reports import BaseReport from _pytest.reports import CollectReport from _pytest.reports import TestReport - + if TYPE_CHECKING: from typing_extensions import Literal @@ -57,7 +57,7 @@ if TYPE_CHECKING: REPORT_COLLECTING_RESOLUTION = 0.5 - + KNOWN_TYPES = ( "failed", "passed", @@ -72,13 +72,13 @@ KNOWN_TYPES = ( _REPORTCHARS_DEFAULT = "fE" -class MoreQuietAction(argparse.Action): +class MoreQuietAction(argparse.Action): """A modified copy of the argparse count action which counts down and updates the legacy quiet attribute at the same time. - + Used to unify verbosity handling. - """ - + """ + def __init__( self, option_strings: Sequence[str], @@ -88,14 +88,14 @@ class MoreQuietAction(argparse.Action): help: Optional[str] = None, ) -> None: super().__init__( - option_strings=option_strings, - dest=dest, - nargs=0, - default=default, - required=required, - help=help, - ) - + option_strings=option_strings, + dest=dest, + nargs=0, + default=default, + required=required, + help=help, + ) + def __call__( self, parser: argparse.ArgumentParser, @@ -103,23 +103,23 @@ class MoreQuietAction(argparse.Action): values: Union[str, Sequence[object], None], option_string: Optional[str] = None, ) -> None: - new_count = getattr(namespace, self.dest, 0) - 1 - setattr(namespace, self.dest, new_count) - # todo Deprecate config.quiet - namespace.quiet = getattr(namespace, "quiet", 0) + 1 - - + new_count = getattr(namespace, self.dest, 0) - 1 + setattr(namespace, self.dest, new_count) + # todo Deprecate config.quiet + namespace.quiet = getattr(namespace, "quiet", 0) + 1 + + def pytest_addoption(parser: Parser) -> None: - group = parser.getgroup("terminal reporting", "reporting", after="general") - group._addoption( - "-v", - "--verbose", - action="count", - default=0, - dest="verbose", - help="increase verbosity.", + group = parser.getgroup("terminal reporting", "reporting", after="general") + group._addoption( + "-v", + "--verbose", + action="count", + default=0, + dest="verbose", + help="increase verbosity.", ) - group._addoption( + group._addoption( "--no-header", action="store_true", default=False, @@ -134,113 +134,113 @@ def pytest_addoption(parser: Parser) -> None: help="disable summary", ) group._addoption( - "-q", - "--quiet", - action=MoreQuietAction, - default=0, - dest="verbose", - help="decrease verbosity.", + "-q", + "--quiet", + action=MoreQuietAction, + default=0, + dest="verbose", + help="decrease verbosity.", ) - group._addoption( + group._addoption( "--verbosity", dest="verbose", type=int, default=0, help="set verbosity. Default is 0.", - ) - group._addoption( - "-r", - action="store", - dest="reportchars", + ) + group._addoption( + "-r", + action="store", + dest="reportchars", default=_REPORTCHARS_DEFAULT, - metavar="chars", + metavar="chars", help="show extra test summary info as specified by chars: (f)ailed, " "(E)rror, (s)kipped, (x)failed, (X)passed, " "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. " "(w)arnings are enabled by default (see --disable-warnings), " "'N' can be used to reset the list. (default: 'fE').", - ) - group._addoption( - "--disable-warnings", - "--disable-pytest-warnings", - default=False, - dest="disable_warnings", - action="store_true", - help="disable warnings summary", - ) - group._addoption( - "-l", - "--showlocals", - action="store_true", - dest="showlocals", - default=False, - help="show locals in tracebacks (disabled by default).", - ) - group._addoption( - "--tb", - metavar="style", - action="store", - dest="tbstyle", - default="auto", - choices=["auto", "long", "short", "no", "line", "native"], - help="traceback print mode (auto/long/short/line/native/no).", - ) - group._addoption( - "--show-capture", - action="store", - dest="showcapture", - choices=["no", "stdout", "stderr", "log", "all"], - default="all", - help="Controls how captured stdout/stderr/log is shown on failed tests. " - "Default is 'all'.", - ) - group._addoption( - "--fulltrace", - "--full-trace", - action="store_true", - default=False, - help="don't cut any tracebacks (default is to cut).", - ) - group._addoption( - "--color", - metavar="color", - action="store", - dest="color", - default="auto", - choices=["yes", "no", "auto"], - help="color terminal output (yes/no/auto).", - ) + ) + group._addoption( + "--disable-warnings", + "--disable-pytest-warnings", + default=False, + dest="disable_warnings", + action="store_true", + help="disable warnings summary", + ) + group._addoption( + "-l", + "--showlocals", + action="store_true", + dest="showlocals", + default=False, + help="show locals in tracebacks (disabled by default).", + ) + group._addoption( + "--tb", + metavar="style", + action="store", + dest="tbstyle", + default="auto", + choices=["auto", "long", "short", "no", "line", "native"], + help="traceback print mode (auto/long/short/line/native/no).", + ) + group._addoption( + "--show-capture", + action="store", + dest="showcapture", + choices=["no", "stdout", "stderr", "log", "all"], + default="all", + help="Controls how captured stdout/stderr/log is shown on failed tests. " + "Default is 'all'.", + ) + group._addoption( + "--fulltrace", + "--full-trace", + action="store_true", + default=False, + help="don't cut any tracebacks (default is to cut).", + ) + group._addoption( + "--color", + metavar="color", + action="store", + dest="color", + default="auto", + choices=["yes", "no", "auto"], + help="color terminal output (yes/no/auto).", + ) group._addoption( "--code-highlight", default="yes", choices=["yes", "no"], help="Whether code should be highlighted (only if --color is also enabled)", ) - - parser.addini( - "console_output_style", + + parser.addini( + "console_output_style", help='console output: "classic", or with additional progress information ("progress" (percentage) | "count").', - default="progress", - ) - - + default="progress", + ) + + def pytest_configure(config: Config) -> None: - reporter = TerminalReporter(config, sys.stdout) - config.pluginmanager.register(reporter, "terminalreporter") - if config.option.debug or config.option.traceconfig: - - def mywriter(tags, args): - msg = " ".join(map(str, args)) - reporter.write_line("[traceconfig] " + msg) - - config.trace.root.setprocessor("pytest:config", mywriter) - - + reporter = TerminalReporter(config, sys.stdout) + config.pluginmanager.register(reporter, "terminalreporter") + if config.option.debug or config.option.traceconfig: + + def mywriter(tags, args): + msg = " ".join(map(str, args)) + reporter.write_line("[traceconfig] " + msg) + + config.trace.root.setprocessor("pytest:config", mywriter) + + def getreportopt(config: Config) -> str: reportchars: str = config.option.reportchars old_aliases = {"F", "S"} - reportopts = "" + reportopts = "" for char in reportchars: if char in old_aliases: char = char.lower() @@ -258,97 +258,97 @@ def getreportopt(config: Config) -> str: elif config.option.disable_warnings and "w" in reportopts: reportopts = reportopts.replace("w", "") - return reportopts - - + return reportopts + + @hookimpl(trylast=True) # after _pytest.runner def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: letter = "F" - if report.passed: - letter = "." - elif report.skipped: - letter = "s" - + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + outcome: str = report.outcome if report.when in ("collect", "setup", "teardown") and outcome == "failed": outcome = "error" letter = "E" - + return outcome, letter, outcome.upper() -@attr.s +@attr.s class WarningReport: """Simple structure to hold warnings information captured by ``pytest_warning_recorded``. - + :ivar str message: 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``). - """ - + """ + message = attr.ib(type=str) nodeid = attr.ib(type=Optional[str], default=None) fslocation = attr.ib( type=Optional[Union[Tuple[str, int], py.path.local]], default=None ) count_towards_summary = True - + def get_location(self, config: Config) -> Optional[str]: """Return the more user-friendly information about the location of a warning, or None.""" - if self.nodeid: - return self.nodeid - if self.fslocation: - if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: - filename, linenum = self.fslocation[:2] + if self.nodeid: + return self.nodeid + if self.fslocation: + if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: + filename, linenum = self.fslocation[:2] relpath = bestrelpath( config.invocation_params.dir, absolutepath(filename) ) return f"{relpath}:{linenum}" - else: - return str(self.fslocation) - return None - - + else: + return str(self.fslocation) + return None + + @final class TerminalReporter: def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: - import _pytest.config - - self.config = config - self._numcollected = 0 + import _pytest.config + + self.config = config + self._numcollected = 0 self._session: Optional[Session] = None self._showfspath: Optional[bool] = None - + self.stats: Dict[str, List[Any]] = {} self._main_color: Optional[str] = None self._known_types: Optional[List[str]] = None self.startdir = config.invocation_dir self.startpath = config.invocation_params.dir - if file is None: - file = sys.stdout - self._tw = _pytest.config.create_terminal_writer(config, file) - self._screen_width = self._tw.fullwidth + if file is None: + file = sys.stdout + self._tw = _pytest.config.create_terminal_writer(config, file) + self._screen_width = self._tw.fullwidth self.currentfspath: Union[None, Path, str, int] = None - self.reportchars = getreportopt(config) - self.hasmarkup = self._tw.hasmarkup - self.isatty = file.isatty() + self.reportchars = getreportopt(config) + self.hasmarkup = self._tw.hasmarkup + self.isatty = file.isatty() self._progress_nodeids_reported: Set[str] = set() - self._show_progress_info = self._determine_show_progress_info() + self._show_progress_info = self._determine_show_progress_info() self._collect_report_last_write: Optional[float] = None self._already_displayed_warnings: Optional[int] = None self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None - + def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]": """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": - return False - # do not show progress if we are showing fixture setup/teardown + return False + # do not show progress if we are showing fixture setup/teardown if self.config.getoption("setupshow", False): - return False + return False cfg: str = self.config.getini("console_output_style") if cfg == "progress": return "progress" @@ -356,7 +356,7 @@ class TerminalReporter: return "count" else: return False - + @property def verbosity(self) -> int: verbosity: int = self.config.option.verbose @@ -389,64 +389,64 @@ class TerminalReporter: return self.verbosity > 0 def hasopt(self, char: str) -> bool: - char = {"xfailed": "x", "skipped": "s"}.get(char, char) - return char in self.reportchars - + char = {"xfailed": "x", "skipped": "s"}.get(char, char) + return char in self.reportchars + def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: fspath = self.config.rootpath / nodeid.split("::")[0] if self.currentfspath is None or fspath != self.currentfspath: - if self.currentfspath is not None and self._show_progress_info: - self._write_progress_information_filling_space() - self.currentfspath = fspath + if self.currentfspath is not None and self._show_progress_info: + self._write_progress_information_filling_space() + self.currentfspath = fspath relfspath = bestrelpath(self.startpath, fspath) - self._tw.line() + self._tw.line() self._tw.write(relfspath + " ") self._tw.write(res, flush=True, **markup) - + def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None: - if self.currentfspath != prefix: - self._tw.line() - self.currentfspath = prefix - self._tw.write(prefix) - if extra: - self._tw.write(extra, **kwargs) - self.currentfspath = -2 - + if self.currentfspath != prefix: + self._tw.line() + self.currentfspath = prefix + self._tw.write(prefix) + if extra: + self._tw.write(extra, **kwargs) + self.currentfspath = -2 + def ensure_newline(self) -> None: - if self.currentfspath: - self._tw.line() - self.currentfspath = None - + if self.currentfspath: + self._tw.line() + self.currentfspath = None + def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: self._tw.write(content, flush=flush, **markup) - + def flush(self) -> None: self._tw.flush() def write_line(self, line: Union[str, bytes], **markup: bool) -> None: if not isinstance(line, str): line = str(line, errors="replace") - self.ensure_newline() - self._tw.line(line, **markup) - + self.ensure_newline() + self._tw.line(line, **markup) + def rewrite(self, line: str, **markup: bool) -> None: """Rewinds the terminal cursor to the beginning and writes the given line. - + :param erase: If True, will also add spaces until the full terminal width to ensure - previous lines are properly erased. - - The rest of the keyword arguments are markup instructions. - """ - erase = markup.pop("erase", False) - if erase: - fill_count = self._tw.fullwidth - len(line) - 1 - fill = " " * fill_count - else: - fill = "" - line = str(line) - self._tw.write("\r" + line + fill, **markup) - + previous lines are properly erased. + + The rest of the keyword arguments are markup instructions. + """ + erase = markup.pop("erase", False) + if erase: + fill_count = self._tw.fullwidth - len(line) - 1 + fill = " " * fill_count + else: + fill = "" + line = str(line) + self._tw.write("\r" + line + fill, **markup) + def write_sep( self, sep: str, @@ -454,15 +454,15 @@ class TerminalReporter: fullwidth: Optional[int] = None, **markup: bool, ) -> None: - self.ensure_newline() + self.ensure_newline() self._tw.sep(sep, title, fullwidth, **markup) - + def section(self, title: str, sep: str = "=", **kw: bool) -> None: - self._tw.sep(sep, title, **kw) - + self._tw.sep(sep, title, **kw) + def line(self, msg: str, **kw: bool) -> None: - self._tw.line(msg, **kw) - + self._tw.line(msg, **kw) + def _add_stats(self, category: str, items: Sequence[Any]) -> None: set_main_color = category not in self.stats self.stats.setdefault(category, []).extend(items) @@ -471,81 +471,81 @@ class TerminalReporter: def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool: for line in str(excrepr).split("\n"): - self.write_line("INTERNALERROR> " + line) + self.write_line("INTERNALERROR> " + line) return True - + def pytest_warning_recorded( self, warning_message: warnings.WarningMessage, nodeid: str, ) -> None: - from _pytest.warnings import warning_record_to_str - - fslocation = warning_message.filename, warning_message.lineno - message = warning_record_to_str(warning_message) - - warning_report = WarningReport( - fslocation=fslocation, message=message, nodeid=nodeid - ) + from _pytest.warnings import warning_record_to_str + + fslocation = warning_message.filename, warning_message.lineno + message = warning_record_to_str(warning_message) + + warning_report = WarningReport( + fslocation=fslocation, message=message, nodeid=nodeid + ) self._add_stats("warnings", [warning_report]) - + def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: - if self.config.option.traceconfig: + if self.config.option.traceconfig: msg = f"PLUGIN registered: {plugin}" # 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. - self.write_line(msg) - + self.write_line(msg) + def pytest_deselected(self, items: Sequence[Item]) -> None: self._add_stats("deselected", items) - + def pytest_runtest_logstart( self, nodeid: str, location: Tuple[str, Optional[int], str] ) -> None: # Ensure that the path is printed before the # 1st test of a module starts running. - if self.showlongtestinfo: - line = self._locationline(nodeid, *location) - self.write_ensure_prefix(line, "") + if self.showlongtestinfo: + line = self._locationline(nodeid, *location) + self.write_ensure_prefix(line, "") self.flush() - elif self.showfspath: + elif self.showfspath: self.write_fspath_result(nodeid, "") self.flush() - + def pytest_runtest_logreport(self, report: TestReport) -> None: self._tests_ran = True - rep = report + rep = report res: Tuple[ str, str, Union[str, Tuple[str, Mapping[str, bool]]] ] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) - category, letter, word = res + category, letter, word = res if not isinstance(word, tuple): markup = None else: - word, markup = word + word, markup = word self._add_stats(category, [rep]) - if not letter and not word: + if not letter and not word: # Probably passed setup/teardown. - return - running_xdist = hasattr(rep, "node") - if markup is None: + return + running_xdist = hasattr(rep, "node") + if markup is None: was_xfail = hasattr(report, "wasxfail") if rep.passed and not was_xfail: - markup = {"green": True} + markup = {"green": True} elif rep.passed and was_xfail: markup = {"yellow": True} - elif rep.failed: - markup = {"red": True} - elif rep.skipped: - markup = {"yellow": True} - else: - markup = {} - if self.verbosity <= 0: + elif rep.failed: + markup = {"red": True} + elif rep.skipped: + markup = {"yellow": True} + else: + markup = {} + if self.verbosity <= 0: self._tw.write(letter, **markup) - else: - self._progress_nodeids_reported.add(rep.nodeid) - line = self._locationline(rep.nodeid, *rep.location) - if not running_xdist: - self.write_ensure_prefix(line, word, **markup) + else: + self._progress_nodeids_reported.add(rep.nodeid) + line = self._locationline(rep.nodeid, *rep.location) + if not running_xdist: + self.write_ensure_prefix(line, word, **markup) if rep.skipped or hasattr(report, "wasxfail"): available_width = ( (self._tw.fullwidth - self._tw.width_of_current_line) @@ -556,22 +556,22 @@ class TerminalReporter: reason_ = _format_trimmed(" ({})", reason, available_width) if reason and reason_ is not None: self._tw.write(reason_) - if self._show_progress_info: - self._write_progress_information_filling_space() - else: - self.ensure_newline() + if self._show_progress_info: + self._write_progress_information_filling_space() + else: + self.ensure_newline() self._tw.write("[%s]" % rep.node.gateway.id) - if self._show_progress_info: - self._tw.write( - self._get_progress_information_message() + " ", cyan=True - ) - else: - self._tw.write(" ") - self._tw.write(word, **markup) - self._tw.write(" " + line) - self.currentfspath = -2 + if self._show_progress_info: + self._tw.write( + self._get_progress_information_message() + " ", cyan=True + ) + else: + self._tw.write(" ") + self._tw.write(word, **markup) + self._tw.write(" " + line) + self.currentfspath = -2 self.flush() - + @property def _is_last_item(self) -> bool: assert self._session is not None @@ -585,114 +585,114 @@ class TerminalReporter: progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests))) else: progress_length = len(" [100%]") - - self._progress_nodeids_reported.add(nodeid) + + self._progress_nodeids_reported.add(nodeid) if self._is_last_item: - self._write_progress_information_filling_space() - else: + self._write_progress_information_filling_space() + else: main_color, _ = self._get_main_color() - w = self._width_of_current_line - past_edge = w + progress_length + 1 >= self._screen_width - if past_edge: - msg = self._get_progress_information_message() + w = self._width_of_current_line + past_edge = w + progress_length + 1 >= self._screen_width + if past_edge: + msg = self._get_progress_information_message() self._tw.write(msg + "\n", **{main_color: True}) - + def _get_progress_information_message(self) -> str: assert self._session - collected = self._session.testscollected + collected = self._session.testscollected if self._show_progress_info == "count": - if collected: - progress = self._progress_nodeids_reported - counter_format = "{{:{}d}}".format(len(str(collected))) + if collected: + progress = self._progress_nodeids_reported + counter_format = "{{:{}d}}".format(len(str(collected))) format_string = f" [{counter_format}/{{}}]" - return format_string.format(len(progress), collected) + return format_string.format(len(progress), collected) return f" [ {collected} / {collected} ]" - else: - if collected: + else: + if collected: return " [{:3d}%]".format( len(self._progress_nodeids_reported) * 100 // collected ) - return " [100%]" - + return " [100%]" + def _write_progress_information_filling_space(self) -> None: color, _ = self._get_main_color() - msg = self._get_progress_information_message() - w = self._width_of_current_line - fill = self._tw.fullwidth - w - 1 + msg = self._get_progress_information_message() + w = self._width_of_current_line + fill = self._tw.fullwidth - w - 1 self.write(msg.rjust(fill), flush=True, **{color: True}) - - @property + + @property def _width_of_current_line(self) -> int: """Return the width of the current line.""" return self._tw.width_of_current_line - + def pytest_collection(self) -> None: - if self.isatty: - if self.config.option.verbose >= 0: + if self.isatty: + if self.config.option.verbose >= 0: self.write("collecting ... ", flush=True, bold=True) self._collect_report_last_write = timing.time() - elif self.config.option.verbose >= 1: + elif self.config.option.verbose >= 1: self.write("collecting ... ", flush=True, bold=True) - + def pytest_collectreport(self, report: CollectReport) -> None: - if report.failed: + if report.failed: self._add_stats("error", [report]) - elif report.skipped: + elif report.skipped: self._add_stats("skipped", [report]) items = [x for x in report.result if isinstance(x, Item)] - self._numcollected += len(items) - if self.isatty: - self.report_collect() - + self._numcollected += len(items) + if self.isatty: + self.report_collect() + def report_collect(self, final: bool = False) -> None: - if self.config.option.verbose < 0: - return - - if not final: - # Only write "collecting" report every 0.5s. + if self.config.option.verbose < 0: + return + + if not final: + # Only write "collecting" report every 0.5s. t = timing.time() - if ( - self._collect_report_last_write is not None + if ( + self._collect_report_last_write is not None and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION - ): - return - self._collect_report_last_write = t - - errors = len(self.stats.get("error", [])) - skipped = len(self.stats.get("skipped", [])) - deselected = len(self.stats.get("deselected", [])) + ): + return + self._collect_report_last_write = t + + errors = len(self.stats.get("error", [])) + skipped = len(self.stats.get("skipped", [])) + deselected = len(self.stats.get("deselected", [])) selected = self._numcollected - errors - skipped - deselected - if final: - line = "collected " - else: - line = "collecting " - line += ( - str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") - ) - if errors: + if final: + line = "collected " + else: + line = "collecting " + line += ( + str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") + ) + if errors: line += " / %d error%s" % (errors, "s" if errors != 1 else "") - if deselected: - line += " / %d deselected" % deselected - if skipped: - line += " / %d skipped" % skipped + if deselected: + line += " / %d deselected" % deselected + if skipped: + line += " / %d skipped" % skipped if self._numcollected > selected > 0: line += " / %d selected" % selected - if self.isatty: - self.rewrite(line, bold=True, erase=True) - if final: - self.write("\n") - else: - self.write_line(line) - + if self.isatty: + self.rewrite(line, bold=True, erase=True) + if final: + self.write("\n") + else: + self.write_line(line) + @hookimpl(trylast=True) def pytest_sessionstart(self, session: "Session") -> None: - self._session = session + self._session = session self._sessionstarttime = timing.time() - if not self.showheader: - return - self.write_sep("=", "test session starts", bold=True) - verinfo = platform.python_version() + if not self.showheader: + return + self.write_sep("=", "test session starts", bold=True) + verinfo = platform.python_version() if not self.no_header: msg = f"platform {sys.platform} -- Python {verinfo}" pypy_version_info = getattr(sys, "pypy_version_info", None) @@ -713,7 +713,7 @@ class TerminalReporter: config=self.config, startdir=self.startdir ) self._write_report_lines_from_hooks(lines) - + def _write_report_lines_from_hooks( self, lines: Sequence[Union[str, Sequence[str]]] ) -> None: @@ -723,32 +723,32 @@ class TerminalReporter: else: for line in line_or_lines: self.write_line(line) - + def pytest_report_header(self, config: Config) -> List[str]: line = "rootdir: %s" % config.rootpath if config.inipath: line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) - + testpaths: List[str] = config.getini("testpaths") if config.invocation_params.dir == config.rootpath and config.args == testpaths: line += ", testpaths: {}".format(", ".join(testpaths)) result = [line] - plugininfo = config.pluginmanager.list_plugin_distinfo() - if plugininfo: + plugininfo = config.pluginmanager.list_plugin_distinfo() + if plugininfo: result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo))) return result - + def pytest_collection_finish(self, session: "Session") -> None: self.report_collect(True) - - lines = self.config.hook.pytest_report_collectionfinish( - config=self.config, startdir=self.startdir, items=session.items - ) - self._write_report_lines_from_hooks(lines) - + + lines = self.config.hook.pytest_report_collectionfinish( + config=self.config, startdir=self.startdir, items=session.items + ) + self._write_report_lines_from_hooks(lines) + if self.config.getoption("collectonly"): if session.items: if self.config.option.verbose > -1: @@ -763,30 +763,30 @@ class TerminalReporter: def _printcollecteditems(self, items: Sequence[Item]) -> None: # 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. - if self.config.option.verbose < 0: - if self.config.option.verbose < -1: + if self.config.option.verbose < 0: + if self.config.option.verbose < -1: counts = Counter(item.nodeid.split("::", 1)[0] for item in items) - for name, count in sorted(counts.items()): - self._tw.line("%s: %d" % (name, count)) - else: - for item in items: - self._tw.line(item.nodeid) - return + for name, count in sorted(counts.items()): + self._tw.line("%s: %d" % (name, count)) + else: + for item in items: + self._tw.line(item.nodeid) + return stack: List[Node] = [] - indent = "" - for item in items: - needed_collectors = item.listchain()[1:] # strip root node - while stack: - if stack == needed_collectors[: len(stack)]: - break - stack.pop() - for col in needed_collectors[len(stack) :]: - stack.append(col) - if col.name == "()": # Skip Instances. - continue - indent = (len(stack) - 1) * " " + indent = "" + for item in items: + needed_collectors = item.listchain()[1:] # strip root node + while stack: + if stack == needed_collectors[: len(stack)]: + break + stack.pop() + for col in needed_collectors[len(stack) :]: + stack.append(col) + if col.name == "()": # Skip Instances. + continue + indent = (len(stack) - 1) * " " self._tw.line(f"{indent}{col}") if self.config.option.verbose >= 1: obj = getattr(col, "obj", None) @@ -794,134 +794,134 @@ class TerminalReporter: if doc: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) - + @hookimpl(hookwrapper=True) def pytest_sessionfinish( self, session: "Session", exitstatus: Union[int, ExitCode] ): - outcome = yield - outcome.get_result() - self._tw.line("") - summary_exit_codes = ( + outcome = yield + outcome.get_result() + self._tw.line("") + summary_exit_codes = ( ExitCode.OK, ExitCode.TESTS_FAILED, ExitCode.INTERRUPTED, ExitCode.USAGE_ERROR, ExitCode.NO_TESTS_COLLECTED, - ) + ) if exitstatus in summary_exit_codes and not self.no_summary: - self.config.hook.pytest_terminal_summary( + self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config - ) + ) if session.shouldfail: self.write_sep("!", str(session.shouldfail), red=True) if exitstatus == ExitCode.INTERRUPTED: - self._report_keyboardinterrupt() + self._report_keyboardinterrupt() self._keyboardinterrupt_memo = None elif session.shouldstop: self.write_sep("!", str(session.shouldstop), red=True) - self.summary_stats() - + self.summary_stats() + @hookimpl(hookwrapper=True) def pytest_terminal_summary(self) -> Generator[None, None, None]: - self.summary_errors() - self.summary_failures() - self.summary_warnings() + self.summary_errors() + self.summary_failures() + self.summary_warnings() self.summary_passes() - yield + yield self.short_test_summary() - # Display any extra warnings from teardown here (if any). - self.summary_warnings() - + # Display any extra warnings from teardown here (if any). + self.summary_warnings() + def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: - self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) - + self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) + def pytest_unconfigure(self) -> None: if self._keyboardinterrupt_memo is not None: - self._report_keyboardinterrupt() - + self._report_keyboardinterrupt() + def _report_keyboardinterrupt(self) -> None: - excrepr = self._keyboardinterrupt_memo + excrepr = self._keyboardinterrupt_memo assert excrepr is not None assert excrepr.reprcrash is not None - msg = excrepr.reprcrash.message - self.write_sep("!", msg) - if "KeyboardInterrupt" in msg: - if self.config.option.fulltrace: - excrepr.toterminal(self._tw) - else: - excrepr.reprcrash.toterminal(self._tw) - self._tw.line( + msg = excrepr.reprcrash.message + self.write_sep("!", msg) + if "KeyboardInterrupt" in msg: + if self.config.option.fulltrace: + excrepr.toterminal(self._tw) + else: + excrepr.reprcrash.toterminal(self._tw) + self._tw.line( "(to show a full traceback on KeyboardInterrupt use --full-trace)", - yellow=True, - ) - - def _locationline(self, nodeid, fspath, lineno, domain): - def mkrel(nodeid): - line = self.config.cwd_relative_nodeid(nodeid) - if domain and line.endswith(domain): - line = line[: -len(domain)] - values = domain.split("[") - values[0] = values[0].replace(".", "::") # don't replace '.' in params - line += "[".join(values) - return line - + yellow=True, + ) + + def _locationline(self, nodeid, fspath, lineno, domain): + def mkrel(nodeid): + line = self.config.cwd_relative_nodeid(nodeid) + if domain and line.endswith(domain): + line = line[: -len(domain)] + values = domain.split("[") + values[0] = values[0].replace(".", "::") # don't replace '.' in params + line += "[".join(values) + return line + # collect_fspath comes from testid which has a "/"-normalized path. - - if fspath: - res = mkrel(nodeid) - if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( - "\\", nodes.SEP - ): + + if fspath: + res = mkrel(nodeid) + if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( + "\\", nodes.SEP + ): res += " <- " + bestrelpath(self.startpath, fspath) - else: - res = "[location]" - return res + " " - - def _getfailureheadline(self, rep): + else: + res = "[location]" + return res + " " + + def _getfailureheadline(self, rep): head_line = rep.head_line if head_line: return head_line return "test session" # XXX? - - def _getcrashline(self, rep): - try: - return str(rep.longrepr.reprcrash) - except AttributeError: - try: - return str(rep.longrepr)[:50] - except AttributeError: - return "" - - # + + def _getcrashline(self, rep): + try: + return str(rep.longrepr.reprcrash) + except AttributeError: + try: + return str(rep.longrepr)[:50] + except AttributeError: + return "" + + # # Summaries for sessionfinish. - # + # def getreports(self, name: str): - values = [] - for x in self.stats.get(name, []): - if not hasattr(x, "_pdbshown"): - values.append(x) - return values - + values = [] + for x in self.stats.get(name, []): + if not hasattr(x, "_pdbshown"): + values.append(x) + return values + def summary_warnings(self) -> None: - if self.hasopt("w"): + if self.hasopt("w"): all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings") - if not all_warnings: - return - + if not all_warnings: + return + final = self._already_displayed_warnings is not None - if final: + if final: warning_reports = all_warnings[self._already_displayed_warnings :] - else: + else: warning_reports = all_warnings self._already_displayed_warnings = len(warning_reports) if not warning_reports: - return - + return + reports_grouped_by_message: Dict[str, List[WarningReport]] = {} for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) - + def collapsed_location_report(reports: List[WarningReport]) -> str: locations = [] for w in reports: @@ -940,8 +940,8 @@ class TerminalReporter: for k, v in counts_by_filename.items() ) - title = "warnings summary (final)" if final else "warnings summary" - self.write_sep("=", title, yellow=True, bold=False) + title = "warnings summary (final)" if final else "warnings summary" + self.write_sep("=", title, yellow=True, bold=False) for message, message_reports in reports_grouped_by_message.items(): maybe_location = collapsed_location_report(message_reports) if maybe_location: @@ -952,23 +952,23 @@ class TerminalReporter: else: message = message.rstrip() self._tw.line(message) - self._tw.line() + self._tw.line() self._tw.line("-- Docs: https://docs.pytest.org/en/stable/warnings.html") - + def summary_passes(self) -> None: - if self.config.option.tbstyle != "no": - if self.hasopt("P"): + if self.config.option.tbstyle != "no": + if self.hasopt("P"): reports: List[TestReport] = self.getreports("passed") - if not reports: - return - self.write_sep("=", "PASSES") - for rep in reports: - if rep.sections: - msg = self._getfailureheadline(rep) + if not reports: + return + self.write_sep("=", "PASSES") + for rep in reports: + if rep.sections: + msg = self._getfailureheadline(rep) self.write_sep("_", msg, green=True, bold=True) - self._outrep_summary(rep) + self._outrep_summary(rep) self._handle_teardown_sections(rep.nodeid) - + def _get_teardown_reports(self, nodeid: str) -> List[TestReport]: reports = self.getreports("") return [ @@ -982,63 +982,63 @@ class TerminalReporter: self.print_teardown_sections(report) def print_teardown_sections(self, rep: TestReport) -> None: - showcapture = self.config.option.showcapture - if showcapture == "no": - return - for secname, content in rep.sections: - if showcapture != "all" and showcapture not in secname: - continue - if "teardown" in secname: - self._tw.sep("-", secname) - if content[-1:] == "\n": - content = content[:-1] - self._tw.line(content) - + showcapture = self.config.option.showcapture + if showcapture == "no": + return + for secname, content in rep.sections: + if showcapture != "all" and showcapture not in secname: + continue + if "teardown" in secname: + self._tw.sep("-", secname) + if content[-1:] == "\n": + content = content[:-1] + self._tw.line(content) + def summary_failures(self) -> None: - if self.config.option.tbstyle != "no": + if self.config.option.tbstyle != "no": reports: List[BaseReport] = self.getreports("failed") - if not reports: - return - self.write_sep("=", "FAILURES") + if not reports: + return + self.write_sep("=", "FAILURES") if self.config.option.tbstyle == "line": for rep in reports: - line = self._getcrashline(rep) - self.write_line(line) + line = self._getcrashline(rep) + self.write_line(line) else: for rep in reports: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) self._handle_teardown_sections(rep.nodeid) - + def summary_errors(self) -> None: - if self.config.option.tbstyle != "no": + if self.config.option.tbstyle != "no": reports: List[BaseReport] = self.getreports("error") - if not reports: - return - self.write_sep("=", "ERRORS") - for rep in self.stats["error"]: - msg = self._getfailureheadline(rep) + if not reports: + return + self.write_sep("=", "ERRORS") + for rep in self.stats["error"]: + msg = self._getfailureheadline(rep) if rep.when == "collect": - msg = "ERROR collecting " + msg + msg = "ERROR collecting " + msg else: msg = f"ERROR at {rep.when} of {msg}" - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) - + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + def _outrep_summary(self, rep: BaseReport) -> None: - rep.toterminal(self._tw) - showcapture = self.config.option.showcapture - if showcapture == "no": - return - for secname, content in rep.sections: - if showcapture != "all" and showcapture not in secname: - continue - self._tw.sep("-", secname) - if content[-1:] == "\n": - content = content[:-1] - self._tw.line(content) - + rep.toterminal(self._tw) + showcapture = self.config.option.showcapture + if showcapture == "no": + return + for secname, content in rep.sections: + if showcapture != "all" and showcapture not in secname: + continue + self._tw.sep("-", secname) + if content[-1:] == "\n": + content = content[:-1] + self._tw.line(content) + def summary_stats(self) -> None: if self.verbosity < -1: return @@ -1046,7 +1046,7 @@ class TerminalReporter: session_duration = timing.time() - self._sessionstarttime (parts, main_color) = self.build_summary_stats_line() line_parts = [] - + display_sep = self.verbosity >= 0 if display_sep: fullwidth = self._tw.fullwidth @@ -1056,7 +1056,7 @@ class TerminalReporter: fullwidth += len(with_markup) - len(text) line_parts.append(with_markup) msg = ", ".join(line_parts) - + main_markup = {main_color: True} duration = " in {}".format(format_session_duration(session_duration)) duration_with_markup = self._tw.markup(duration, **main_markup) @@ -1079,7 +1079,7 @@ class TerminalReporter: def short_test_summary(self) -> None: if not self.reportchars: return - + def show_simple(stat, lines: List[str]) -> None: failed = self.stats.get(stat, []) if not failed: @@ -1295,7 +1295,7 @@ def _get_line_with_reprcrash_message( line = f"{verbose_word} {pos}" line_width = wcswidth(line) - try: + try: # Type ignored intentionally -- possible AttributeError expected. msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] except AttributeError: @@ -1305,9 +1305,9 @@ def _get_line_with_reprcrash_message( msg = _format_trimmed(" - {}", msg, available_width) if msg is not None: line += msg - + return line - + def _folded_skips( startpath: Path, skipped: Sequence[CollectReport], @@ -1346,33 +1346,33 @@ _color_for_type = { "passed": "green", } _color_for_type_default = "yellow" - - + + def pluralize(count: int, noun: str) -> Tuple[int, str]: # No need to pluralize words such as `failed` or `passed`. if noun not in ["error", "warnings", "test"]: return count, noun - + # The `warnings` key is plural. To avoid API breakage, we keep it that way but # set it to singular here so we can determine plurality in the same way as we do # for `error`. noun = noun.replace("warnings", "warning") - + return count, noun + "s" if count != 1 else noun - + def _plugin_nameversions(plugininfo) -> List[str]: values: List[str] = [] - for plugin, dist in plugininfo: + for plugin, dist in plugininfo: # 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. - if name.startswith("pytest-"): - name = name[7:] + if name.startswith("pytest-"): + name = name[7:] # We decided to print python package names they can have more than one plugin. - if name not in values: - values.append(name) - return values + if name not in values: + values.append(name) + return values def format_session_duration(seconds: float) -> str: diff --git a/contrib/python/pytest/py3/_pytest/tmpdir.py b/contrib/python/pytest/py3/_pytest/tmpdir.py index 679e5d0cfd..a6bd383a9c 100644 --- a/contrib/python/pytest/py3/_pytest/tmpdir.py +++ b/contrib/python/pytest/py3/_pytest/tmpdir.py @@ -1,38 +1,38 @@ """Support for providing temporary directories to test functions.""" -import os -import re +import os +import re import sys -import tempfile +import tempfile from pathlib import Path from typing import Optional - -import attr -import py - -from .pathlib import LOCK_TIMEOUT -from .pathlib import make_numbered_dir -from .pathlib import make_numbered_dir_with_cleanup + +import attr +import py + +from .pathlib import LOCK_TIMEOUT +from .pathlib import make_numbered_dir +from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf from _pytest.compat import final from _pytest.config import Config from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest -from _pytest.monkeypatch import MonkeyPatch - - +from _pytest.monkeypatch import MonkeyPatch + + @final @attr.s(init=False) 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. """ - + _given_basetemp = attr.ib(type=Optional[Path]) - _trace = attr.ib() + _trace = attr.ib() _basetemp = attr.ib(type=Optional[Path]) - + def __init__( self, given_basetemp: Optional[Path], @@ -52,21 +52,21 @@ class TempPathFactory: self._trace = trace self._basetemp = basetemp - @classmethod + @classmethod def from_config( cls, config: Config, *, _ispytest: bool = False, ) -> "TempPathFactory": """Create a factory according to pytest configuration. :meta private: - """ + """ check_ispytest(_ispytest) - return cls( + return cls( given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir"), _ispytest=True, - ) - + ) + def _ensure_relative_to_basetemp(self, basename: str) -> str: basename = os.path.normpath(basename) if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp(): @@ -89,19 +89,19 @@ class TempPathFactory: The path to the new directory. """ basename = self._ensure_relative_to_basetemp(basename) - if not numbered: - p = self.getbasetemp().joinpath(basename) + if not numbered: + p = self.getbasetemp().joinpath(basename) p.mkdir(mode=0o700) - else: + else: p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700) - self._trace("mktemp", p) - return p - + self._trace("mktemp", p) + return p + def getbasetemp(self) -> Path: """Return the base temporary directory, creating it if needed.""" if self._basetemp is not None: - return self._basetemp - + return self._basetemp + if self._given_basetemp is not None: basetemp = self._given_basetemp if basetemp.exists(): @@ -146,16 +146,16 @@ class TempPathFactory: self._basetemp = basetemp self._trace("new basetemp", basetemp) return basetemp - + @final @attr.s(init=False) class TempdirFactory: """Backward comptibility wrapper that implements :class:``py.path.local`` for :class:``TempPathFactory``.""" - + _tmppath_factory = attr.ib(type=TempPathFactory) - + def __init__( self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False ) -> None: @@ -164,67 +164,67 @@ class TempdirFactory: def mktemp(self, basename: str, numbered: bool = True) -> py.path.local: """Same as :meth:`TempPathFactory.mktemp`, 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: """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]: - """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).""" - import getpass - - try: - return getpass.getuser() - except (ImportError, KeyError): - return None - - + import getpass + + try: + return getpass.getuser() + except (ImportError, KeyError): + return None + + def pytest_configure(config: Config) -> None: - """Create a TempdirFactory and attach it to the config object. - - This is to comply with existing plugins which expect the handler to be - available at pytest_configure time, but ideally should be moved entirely - to the tmpdir_factory session fixture. - """ - mp = MonkeyPatch() + """Create a TempdirFactory and attach it to the config object. + + This is to comply with existing plugins which expect the handler to be + available at pytest_configure time, but ideally should be moved entirely + to the tmpdir_factory session fixture. + """ + mp = MonkeyPatch() tmppath_handler = TempPathFactory.from_config(config, _ispytest=True) t = TempdirFactory(tmppath_handler, _ispytest=True) - config._cleanup.append(mp.undo) - mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) - mp.setattr(config, "_tmpdirhandler", t, raising=False) - - + config._cleanup.append(mp.undo) + mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) + mp.setattr(config, "_tmpdirhandler", t, raising=False) + + @fixture(scope="session") def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. return request.config._tmpdirhandler # type: ignore - - + + @fixture(scope="session") def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.""" # Set dynamically by pytest_configure() above. return request.config._tmp_path_factory # type: ignore - - + + def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: - name = request.node.name - name = re.sub(r"[\W]", "_", name) - MAXVAL = 30 - name = name[:MAXVAL] - return factory.mktemp(name, numbered=True) - - + name = request.node.name + name = re.sub(r"[\W]", "_", name) + MAXVAL = 30 + name = name[:MAXVAL] + return factory.mktemp(name, numbered=True) + + @fixture def tmpdir(tmp_path: Path) -> py.path.local: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. - + By default, a new base temporary directory is created each test session, and old bases are removed after 3 sessions, to aid in debugging. If ``--basetemp`` is used then it is cleared each session. See :ref:`base @@ -232,23 +232,23 @@ def tmpdir(tmp_path: Path) -> py.path.local: 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 + """ return py.path.local(tmp_path) - - + + @fixture def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. - + By default, a new base temporary directory is created each test session, and old bases are removed after 3 sessions, to aid in debugging. If ``--basetemp`` is used then it is cleared each session. See :ref:`base temporary directory`. - + The returned object is a :class:`pathlib.Path` object. - """ - - return _mk_tmp(request, tmp_path_factory) + """ + + return _mk_tmp(request, tmp_path_factory) diff --git a/contrib/python/pytest/py3/_pytest/unittest.py b/contrib/python/pytest/py3/_pytest/unittest.py index f4471b7922..55f15efe4b 100644 --- a/contrib/python/pytest/py3/_pytest/unittest.py +++ b/contrib/python/pytest/py3/_pytest/unittest.py @@ -1,6 +1,6 @@ """Discover and run std-library "unittest" style tests.""" -import sys -import traceback +import sys +import traceback import types from typing import Any from typing import Callable @@ -12,29 +12,29 @@ from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union - -import _pytest._code + +import _pytest._code import pytest -from _pytest.compat import getimfunc +from _pytest.compat import getimfunc from _pytest.compat import is_async_function -from _pytest.config import hookimpl +from _pytest.config import hookimpl from _pytest.fixtures import FixtureRequest from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import exit -from _pytest.outcomes import fail -from _pytest.outcomes import skip -from _pytest.outcomes import xfail -from _pytest.python import Class -from _pytest.python import Function +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import xfail +from _pytest.python import Class +from _pytest.python import Function from _pytest.python import PyCollector from _pytest.runner import CallInfo from _pytest.skipping import skipped_by_mark_key from _pytest.skipping import unexpectedsuccess_key - + if TYPE_CHECKING: import unittest - + from _pytest.fixtures import _Scope _SysExcInfoType = Union[ @@ -47,54 +47,54 @@ def pytest_pycollect_makeitem( collector: PyCollector, name: str, obj: object ) -> Optional["UnitTestCase"]: # Has unittest been imported and is obj a subclass of its TestCase? - try: + try: ut = sys.modules["unittest"] # Type ignored because `ut` is an opaque module. if not issubclass(obj, ut.TestCase): # type: ignore return None - except Exception: + except Exception: return None # Yes, so let's collect it. item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj) return item - - -class UnitTestCase(Class): + + +class UnitTestCase(Class): # Marker for fixturemanger.getfixtureinfo() # to declare that our children do not support funcargs. - nofuncargs = True - + nofuncargs = True + def collect(self) -> Iterable[Union[Item, Collector]]: - from unittest import TestLoader - - cls = self.obj - if not getattr(cls, "__test__", True): - return + from unittest import TestLoader + + cls = self.obj + if not getattr(cls, "__test__", True): + return skipped = _is_skipped(cls) if not skipped: self._inject_setup_teardown_fixtures(cls) self._inject_setup_class_fixture() - self.session._fixturemanager.parsefactories(self, unittest=True) - loader = TestLoader() - foundsomething = False - for name in loader.getTestCaseNames(self.obj): - x = getattr(self.obj, name) - if not getattr(x, "__test__", True): - continue - funcobj = getimfunc(x) + self.session._fixturemanager.parsefactories(self, unittest=True) + loader = TestLoader() + foundsomething = False + for name in loader.getTestCaseNames(self.obj): + x = getattr(self.obj, name) + if not getattr(x, "__test__", True): + continue + funcobj = getimfunc(x) yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj) - foundsomething = True - - if not foundsomething: - runtest = getattr(self.obj, "runTest", None) - if runtest is not None: - ut = sys.modules.get("twisted.trial.unittest", None) + foundsomething = True + + if not foundsomething: + runtest = getattr(self.obj, "runTest", None) + if runtest is not None: + ut = sys.modules.get("twisted.trial.unittest", None) # Type ignored because `ut` is an opaque module. if ut is None or runtest != ut.TestCase.runTest: # type: ignore yield TestCaseFunction.from_parent(self, name="runTest") - + def _inject_setup_teardown_fixtures(self, cls: type) -> None: """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding teardown functions (#517).""" @@ -108,7 +108,7 @@ class UnitTestCase(Class): ) if class_fixture: cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] - + method_fixture = _make_xunit_fixture( cls, "setup_method", @@ -182,63 +182,63 @@ def _make_xunit_fixture( return fixture -class TestCaseFunction(Function): - nofuncargs = True +class TestCaseFunction(Function): + nofuncargs = True _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None _testcase: Optional["unittest.TestCase"] = None - + def setup(self) -> None: # A bound method to be called during teardown() if set (see 'runtest()'). self._explicit_tearDown: Optional[Callable[[], None]] = None assert self.parent is not None self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] - self._obj = getattr(self._testcase, self.name) - if hasattr(self, "_request"): - self._request._fillfixtures() - + self._obj = getattr(self._testcase, self.name) + if hasattr(self, "_request"): + self._request._fillfixtures() + def teardown(self) -> None: if self._explicit_tearDown is not None: self._explicit_tearDown() self._explicit_tearDown = None - self._testcase = None - self._obj = None - + self._testcase = None + self._obj = None + def startTest(self, testcase: "unittest.TestCase") -> None: - pass - + pass + def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: # Unwrap potential exception info (see twisted trial support below). - rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) - try: + rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) + try: excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # type: ignore[arg-type] # Invoke the attributes to trigger storing the traceback # trial causes some issue there. excinfo.value excinfo.traceback - except TypeError: - try: - try: - values = traceback.format_exception(*rawexcinfo) - values.insert( - 0, - "NOTE: Incompatible Exception Representation, " - "displaying natively:\n\n", - ) - fail("".join(values), pytrace=False) - except (fail.Exception, KeyboardInterrupt): - raise + except TypeError: + try: + try: + values = traceback.format_exception(*rawexcinfo) + values.insert( + 0, + "NOTE: Incompatible Exception Representation, " + "displaying natively:\n\n", + ) + fail("".join(values), pytrace=False) + except (fail.Exception, KeyboardInterrupt): + raise except BaseException: - fail( - "ERROR: Unknown Incompatible Exception " - "representation:\n%r" % (rawexcinfo,), - pytrace=False, - ) - except KeyboardInterrupt: - raise - except fail.Exception: + fail( + "ERROR: Unknown Incompatible Exception " + "representation:\n%r" % (rawexcinfo,), + pytrace=False, + ) + except KeyboardInterrupt: + raise + except fail.Exception: excinfo = _pytest._code.ExceptionInfo.from_current() - self.__dict__.setdefault("_excinfo", []).append(excinfo) - + self.__dict__.setdefault("_excinfo", []).append(excinfo) + def addError( self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" ) -> None: @@ -247,42 +247,42 @@ class TestCaseFunction(Function): exit(rawexcinfo[1].msg) except TypeError: pass - self._addexcinfo(rawexcinfo) - + self._addexcinfo(rawexcinfo) + def addFailure( self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" ) -> None: - self._addexcinfo(rawexcinfo) - + self._addexcinfo(rawexcinfo) + def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None: - try: - skip(reason) - except skip.Exception: + try: + skip(reason) + except skip.Exception: self._store[skipped_by_mark_key] = True - self._addexcinfo(sys.exc_info()) - + self._addexcinfo(sys.exc_info()) + def addExpectedFailure( self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType", reason: str = "", ) -> None: - try: - xfail(str(reason)) - except xfail.Exception: - self._addexcinfo(sys.exc_info()) - + try: + xfail(str(reason)) + except xfail.Exception: + self._addexcinfo(sys.exc_info()) + def addUnexpectedSuccess( self, testcase: "unittest.TestCase", reason: str = "" ) -> None: self._store[unexpectedsuccess_key] = reason - + def addSuccess(self, testcase: "unittest.TestCase") -> None: - pass - + pass + def stopTest(self, testcase: "unittest.TestCase") -> None: - pass - + pass + def _expecting_failure(self, test_method) -> bool: """Return True if the given unittest method (or the entire class) is marked with @expectedFailure.""" @@ -291,7 +291,7 @@ class TestCaseFunction(Function): ) expecting_failure_class = getattr(self, "__unittest_expecting_failure__", False) return bool(expecting_failure_class or expecting_failure_method) - + def runtest(self) -> None: from _pytest.debugging import maybe_wrap_pytest_function_for_tracing @@ -303,7 +303,7 @@ class TestCaseFunction(Function): if is_async_function(self.obj): # Type ignored because self acts as the TestResult, but is not actually one. self._testcase(result=self) # type: ignore[arg-type] - else: + else: # When --pdb is given, we want to postpone calling tearDown() otherwise # when entering the pdb prompt, tearDown() would have probably cleaned up # instance variables, which makes it difficult to debug. @@ -313,7 +313,7 @@ class TestCaseFunction(Function): if self.config.getoption("usepdb") and not _is_skipped(self.obj): self._explicit_tearDown = self._testcase.tearDown setattr(self._testcase, "tearDown", lambda *args: None) - + # We need to update the actual bound method with self.obj, because # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. setattr(self._testcase, self.name, self.obj) @@ -325,24 +325,24 @@ class TestCaseFunction(Function): def _prunetraceback( self, excinfo: _pytest._code.ExceptionInfo[BaseException] ) -> None: - Function._prunetraceback(self, excinfo) - traceback = excinfo.traceback.filter( - lambda x: not x.frame.f_globals.get("__unittest") - ) - if traceback: - excinfo.traceback = traceback - - -@hookimpl(tryfirst=True) + Function._prunetraceback(self, excinfo) + traceback = excinfo.traceback.filter( + lambda x: not x.frame.f_globals.get("__unittest") + ) + if traceback: + excinfo.traceback = traceback + + +@hookimpl(tryfirst=True) def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: - if isinstance(item, TestCaseFunction): - if item._excinfo: - call.excinfo = item._excinfo.pop(0) - try: - del call.result - except AttributeError: - pass - + if isinstance(item, TestCaseFunction): + if item._excinfo: + call.excinfo = item._excinfo.pop(0) + try: + del call.result + except AttributeError: + pass + unittest = sys.modules.get("unittest") if ( unittest @@ -355,49 +355,49 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: lambda: pytest.skip(str(excinfo.value)), call.when ) call.excinfo = call2.excinfo - + # Twisted trial support. - - -@hookimpl(hookwrapper=True) + + +@hookimpl(hookwrapper=True) def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: - if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: + if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: ut: Any = sys.modules["twisted.python.failure"] - Failure__init__ = ut.Failure.__init__ - check_testcase_implements_trial_reporter() - - def excstore( - self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None - ): - if exc_value is None: - self._rawexcinfo = sys.exc_info() - else: - if exc_type is None: - exc_type = type(exc_value) - self._rawexcinfo = (exc_type, exc_value, exc_tb) - try: - Failure__init__( - self, exc_value, exc_type, exc_tb, captureVars=captureVars - ) - except TypeError: - Failure__init__(self, exc_value, exc_type, exc_tb) - - ut.Failure.__init__ = excstore - yield - ut.Failure.__init__ = Failure__init__ - else: - yield - - + Failure__init__ = ut.Failure.__init__ + check_testcase_implements_trial_reporter() + + def excstore( + self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None + ): + if exc_value is None: + self._rawexcinfo = sys.exc_info() + else: + if exc_type is None: + exc_type = type(exc_value) + self._rawexcinfo = (exc_type, exc_value, exc_tb) + try: + Failure__init__( + self, exc_value, exc_type, exc_tb, captureVars=captureVars + ) + except TypeError: + Failure__init__(self, exc_value, exc_type, exc_tb) + + ut.Failure.__init__ = excstore + yield + ut.Failure.__init__ = Failure__init__ + else: + yield + + def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: - if done: - return - from zope.interface import classImplements - from twisted.trial.itrial import IReporter - - classImplements(TestCaseFunction, IReporter) - done.append(1) + if done: + return + from zope.interface import classImplements + from twisted.trial.itrial import IReporter + + classImplements(TestCaseFunction, IReporter) + done.append(1) def _is_skipped(obj) -> bool: diff --git a/contrib/python/pytest/py3/_pytest/warning_types.py b/contrib/python/pytest/py3/_pytest/warning_types.py index 9e045c52cb..2eadd9fe4d 100644 --- a/contrib/python/pytest/py3/_pytest/warning_types.py +++ b/contrib/python/pytest/py3/_pytest/warning_types.py @@ -3,30 +3,30 @@ from typing import Generic from typing import Type from typing import TypeVar -import attr - +import attr + from _pytest.compat import final - -class PytestWarning(UserWarning): + +class PytestWarning(UserWarning): """Base class for all warnings emitted by pytest.""" - + __module__ = "pytest" - + @final class PytestAssertRewriteWarning(PytestWarning): """Warning emitted by the pytest assert rewrite module.""" - + __module__ = "pytest" - + @final class PytestCacheWarning(PytestWarning): """Warning emitted by the cache plugin in various situations.""" - + __module__ = "pytest" - + @final class PytestConfigWarning(PytestWarning): @@ -50,24 +50,24 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning): @final -class PytestExperimentalApiWarning(PytestWarning, FutureWarning): +class PytestExperimentalApiWarning(PytestWarning, FutureWarning): """Warning category used to denote experiments in pytest. - + Use sparingly as the API might change or even be removed completely in a future version. - """ - + """ + __module__ = "pytest" - @classmethod + @classmethod def simple(cls, apiname: str) -> "PytestExperimentalApiWarning": - return cls( - "{apiname} is an experimental api that may change over time".format( - apiname=apiname - ) - ) - - + return cls( + "{apiname} is an experimental api that may change over time".format( + apiname=apiname + ) + ) + + @final class PytestUnhandledCoroutineWarning(PytestWarning): """Warning emitted for an unhandled coroutine. @@ -116,17 +116,17 @@ _W = TypeVar("_W", bound=PytestWarning) @final -@attr.s +@attr.s class UnformattedWarning(Generic[_W]): """A warning meant to be formatted during runtime. - + This is used to hold warnings that need to format their message at runtime, as opposed to a direct message. - """ - + """ + category = attr.ib(type=Type["_W"]) template = attr.ib(type=str) - + def format(self, **kwargs: Any) -> _W: """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)) diff --git a/contrib/python/pytest/py3/_pytest/warnings.py b/contrib/python/pytest/py3/_pytest/warnings.py index 60e9a4fd72..35eed96df5 100644 --- a/contrib/python/pytest/py3/_pytest/warnings.py +++ b/contrib/python/pytest/py3/_pytest/warnings.py @@ -1,31 +1,31 @@ -import sys -import warnings -from contextlib import contextmanager +import sys +import warnings +from contextlib import contextmanager from typing import Generator from typing import Optional from typing import TYPE_CHECKING - -import pytest + +import pytest from _pytest.config import apply_warning_filters from _pytest.config import Config from _pytest.config import parse_warning_filter from _pytest.main import Session from _pytest.nodes import Item from _pytest.terminal import TerminalReporter - + if TYPE_CHECKING: from typing_extensions import Literal - - + + def pytest_configure(config: Config) -> None: - config.addinivalue_line( - "markers", - "filterwarnings(warning): add a warning filter to the given test. " + config.addinivalue_line( + "markers", + "filterwarnings(warning): add a warning filter to the given test. " "see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings ", - ) - - -@contextmanager + ) + + +@contextmanager def catch_warnings_for_item( config: Config, ihook, @@ -33,42 +33,42 @@ def catch_warnings_for_item( item: Optional[Item], ) -> Generator[None, None, None]: """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. + Each warning captured triggers the ``pytest_warning_recorded`` hook. - """ + """ config_filters = config.getini("filterwarnings") cmdline_filters = config.known_args_namespace.pythonwarnings or [] - with warnings.catch_warnings(record=True) as log: + with warnings.catch_warnings(record=True) as log: # mypy can't infer that record=True means log is not None; help it. 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). - warnings.filterwarnings("always", category=DeprecationWarning) - warnings.filterwarnings("always", category=PendingDeprecationWarning) - + warnings.filterwarnings("always", category=DeprecationWarning) + warnings.filterwarnings("always", category=PendingDeprecationWarning) + apply_warning_filters(config_filters, cmdline_filters) - + # apply filters from "filterwarnings" marks nodeid = "" if item is None else item.nodeid - if item is not None: - for mark in item.iter_markers(name="filterwarnings"): - for arg in mark.args: + if item is not None: + for mark in item.iter_markers(name="filterwarnings"): + for arg in mark.args: warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) - - yield - - for warning_message in log: - ihook.pytest_warning_captured.call_historic( + + yield + + for warning_message in log: + ihook.pytest_warning_captured.call_historic( kwargs=dict( warning_message=warning_message, when=when, item=item, location=None, ) - ) + ) ihook.pytest_warning_recorded.call_historic( kwargs=dict( warning_message=warning_message, @@ -77,49 +77,49 @@ def catch_warnings_for_item( location=None, ) ) - - + + def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: """Convert a warnings.WarningMessage to a string.""" - warn_msg = warning_message.message - msg = warnings.formatwarning( + warn_msg = warning_message.message + msg = warnings.formatwarning( str(warn_msg), - warning_message.category, - warning_message.filename, - warning_message.lineno, - warning_message.line, - ) - return msg - - -@pytest.hookimpl(hookwrapper=True, tryfirst=True) + warning_message.category, + warning_message.filename, + warning_message.lineno, + warning_message.line, + ) + return msg + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: - with catch_warnings_for_item( - config=item.config, ihook=item.ihook, when="runtest", item=item - ): - yield - - -@pytest.hookimpl(hookwrapper=True, tryfirst=True) + with catch_warnings_for_item( + config=item.config, ihook=item.ihook, when="runtest", item=item + ): + yield + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_collection(session: Session) -> Generator[None, None, None]: - config = session.config - with catch_warnings_for_item( - config=config, ihook=config.hook, when="collect", item=None - ): - yield - - -@pytest.hookimpl(hookwrapper=True) + config = session.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="collect", item=None + ): + yield + + +@pytest.hookimpl(hookwrapper=True) def pytest_terminal_summary( terminalreporter: TerminalReporter, ) -> Generator[None, None, None]: - config = terminalreporter.config - with catch_warnings_for_item( - config=config, ihook=config.hook, when="config", item=None - ): - yield - - + config = terminalreporter.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ): + yield + + @pytest.hookimpl(hookwrapper=True) def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: config = session.config diff --git a/contrib/python/pytest/py3/pytest/__init__.py b/contrib/python/pytest/py3/pytest/__init__.py index 1307497066..70177f9504 100644 --- a/contrib/python/pytest/py3/pytest/__init__.py +++ b/contrib/python/pytest/py3/pytest/__init__.py @@ -1,121 +1,121 @@ -# PYTHON_ARGCOMPLETE_OK +# PYTHON_ARGCOMPLETE_OK """pytest: unit and functional testing with Python.""" from . import collect -from _pytest import __version__ -from _pytest.assertion import register_assert_rewrite +from _pytest import __version__ +from _pytest.assertion import register_assert_rewrite from _pytest.cacheprovider import Cache from _pytest.capture import CaptureFixture -from _pytest.config import cmdline +from _pytest.config import cmdline from _pytest.config import console_main from _pytest.config import ExitCode -from _pytest.config import hookimpl -from _pytest.config import hookspec -from _pytest.config import main -from _pytest.config import UsageError -from _pytest.debugging import pytestPDB as __pytestPDB +from _pytest.config import hookimpl +from _pytest.config import hookspec +from _pytest.config import main +from _pytest.config import UsageError +from _pytest.debugging import pytestPDB as __pytestPDB from _pytest.fixtures import _fillfuncargs -from _pytest.fixtures import fixture +from _pytest.fixtures import fixture from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest -from _pytest.fixtures import yield_fixture -from _pytest.freeze_support import freeze_includes +from _pytest.fixtures import yield_fixture +from _pytest.freeze_support import freeze_includes from _pytest.logging import LogCaptureFixture -from _pytest.main import Session -from _pytest.mark import MARK_GEN as mark -from _pytest.mark import param +from _pytest.main import Session +from _pytest.mark import MARK_GEN as mark +from _pytest.mark import param from _pytest.monkeypatch import MonkeyPatch -from _pytest.nodes import Collector -from _pytest.nodes import File -from _pytest.nodes import Item -from _pytest.outcomes import exit -from _pytest.outcomes import fail -from _pytest.outcomes import importorskip -from _pytest.outcomes import skip -from _pytest.outcomes import xfail +from _pytest.nodes import Collector +from _pytest.nodes import File +from _pytest.nodes import Item +from _pytest.outcomes import exit +from _pytest.outcomes import fail +from _pytest.outcomes import importorskip +from _pytest.outcomes import skip +from _pytest.outcomes import xfail from _pytest.pytester import Pytester from _pytest.pytester import Testdir -from _pytest.python import Class -from _pytest.python import Function -from _pytest.python import Instance -from _pytest.python import Module -from _pytest.python import Package -from _pytest.python_api import approx -from _pytest.python_api import raises -from _pytest.recwarn import deprecated_call +from _pytest.python import Class +from _pytest.python import Function +from _pytest.python import Instance +from _pytest.python import Module +from _pytest.python import Package +from _pytest.python_api import approx +from _pytest.python_api import raises +from _pytest.recwarn import deprecated_call from _pytest.recwarn import WarningsRecorder -from _pytest.recwarn import warns +from _pytest.recwarn import warns from _pytest.tmpdir import TempdirFactory from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestCacheWarning from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestConfigWarning -from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warning_types import PytestExperimentalApiWarning +from _pytest.warning_types import PytestDeprecationWarning +from _pytest.warning_types import PytestExperimentalApiWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnhandledThreadExceptionWarning from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestUnraisableExceptionWarning -from _pytest.warning_types import PytestWarning - -set_trace = __pytestPDB.set_trace - -__all__ = [ - "__version__", - "_fillfuncargs", - "approx", +from _pytest.warning_types import PytestWarning + +set_trace = __pytestPDB.set_trace + +__all__ = [ + "__version__", + "_fillfuncargs", + "approx", "Cache", "CaptureFixture", - "Class", - "cmdline", + "Class", + "cmdline", "collect", - "Collector", + "Collector", "console_main", - "deprecated_call", - "exit", + "deprecated_call", + "exit", "ExitCode", - "fail", - "File", - "fixture", + "fail", + "File", + "fixture", "FixtureLookupError", "FixtureRequest", - "freeze_includes", - "Function", - "hookimpl", - "hookspec", - "importorskip", - "Instance", - "Item", + "freeze_includes", + "Function", + "hookimpl", + "hookspec", + "importorskip", + "Instance", + "Item", "LogCaptureFixture", - "main", - "mark", - "Module", + "main", + "mark", + "Module", "MonkeyPatch", - "Package", - "param", + "Package", + "param", "PytestAssertRewriteWarning", "PytestCacheWarning", "PytestCollectionWarning", "PytestConfigWarning", - "PytestDeprecationWarning", - "PytestExperimentalApiWarning", + "PytestDeprecationWarning", + "PytestExperimentalApiWarning", "Pytester", "PytestUnhandledCoroutineWarning", "PytestUnhandledThreadExceptionWarning", "PytestUnknownMarkWarning", "PytestUnraisableExceptionWarning", - "PytestWarning", - "raises", - "register_assert_rewrite", - "Session", - "set_trace", - "skip", + "PytestWarning", + "raises", + "register_assert_rewrite", + "Session", + "set_trace", + "skip", "TempPathFactory", "Testdir", "TempdirFactory", - "UsageError", + "UsageError", "WarningsRecorder", - "warns", - "xfail", - "yield_fixture", -] + "warns", + "xfail", + "yield_fixture", +] diff --git a/contrib/python/pytest/py3/ya.make b/contrib/python/pytest/py3/ya.make index aeeeef3c15..1d9a6034e4 100644 --- a/contrib/python/pytest/py3/ya.make +++ b/contrib/python/pytest/py3/ya.make @@ -1,20 +1,20 @@ PY3_LIBRARY() - + OWNER(dmitko g:python-contrib) - + VERSION(6.2.5) - + LICENSE(MIT) - -PEERDIR( - contrib/python/attrs + +PEERDIR( + contrib/python/attrs contrib/python/iniconfig contrib/python/packaging - contrib/python/pluggy - contrib/python/py + contrib/python/pluggy + contrib/python/py contrib/python/toml -) - +) + IF (OS_WINDOWS) PEERDIR( contrib/python/atomicwrites @@ -28,10 +28,10 @@ NO_CHECK_IMPORTS( _pytest.* ) -PY_SRCS( - TOP_LEVEL +PY_SRCS( + TOP_LEVEL _pytest/__init__.py - _pytest/_argcomplete.py + _pytest/_argcomplete.py _pytest/_code/__init__.py _pytest/_code/code.py _pytest/_code/source.py @@ -40,62 +40,62 @@ PY_SRCS( _pytest/_io/terminalwriter.py _pytest/_io/wcwidth.py _pytest/_version.py - _pytest/assertion/__init__.py - _pytest/assertion/rewrite.py - _pytest/assertion/truncate.py - _pytest/assertion/util.py - _pytest/cacheprovider.py - _pytest/capture.py - _pytest/compat.py + _pytest/assertion/__init__.py + _pytest/assertion/rewrite.py + _pytest/assertion/truncate.py + _pytest/assertion/util.py + _pytest/cacheprovider.py + _pytest/capture.py + _pytest/compat.py _pytest/config/__init__.py - _pytest/config/argparsing.py - _pytest/config/exceptions.py - _pytest/config/findpaths.py - _pytest/debugging.py - _pytest/deprecated.py - _pytest/doctest.py + _pytest/config/argparsing.py + _pytest/config/exceptions.py + _pytest/config/findpaths.py + _pytest/debugging.py + _pytest/deprecated.py + _pytest/doctest.py _pytest/faulthandler.py - _pytest/fixtures.py - _pytest/freeze_support.py - _pytest/helpconfig.py - _pytest/hookspec.py - _pytest/junitxml.py - _pytest/logging.py - _pytest/main.py + _pytest/fixtures.py + _pytest/freeze_support.py + _pytest/helpconfig.py + _pytest/hookspec.py + _pytest/junitxml.py + _pytest/logging.py + _pytest/main.py _pytest/mark/__init__.py _pytest/mark/expression.py - _pytest/mark/structures.py - _pytest/monkeypatch.py - _pytest/nodes.py - _pytest/nose.py - _pytest/outcomes.py - _pytest/pastebin.py - _pytest/pathlib.py - _pytest/pytester.py + _pytest/mark/structures.py + _pytest/monkeypatch.py + _pytest/nodes.py + _pytest/nose.py + _pytest/outcomes.py + _pytest/pastebin.py + _pytest/pathlib.py + _pytest/pytester.py _pytest/pytester_assertions.py _pytest/python.py - _pytest/python_api.py - _pytest/recwarn.py - _pytest/reports.py - _pytest/runner.py - _pytest/setuponly.py - _pytest/setupplan.py - _pytest/skipping.py - _pytest/stepwise.py + _pytest/python_api.py + _pytest/recwarn.py + _pytest/reports.py + _pytest/runner.py + _pytest/setuponly.py + _pytest/setupplan.py + _pytest/skipping.py + _pytest/stepwise.py _pytest/store.py - _pytest/terminal.py + _pytest/terminal.py _pytest/threadexception.py _pytest/timing.py - _pytest/tmpdir.py - _pytest/unittest.py + _pytest/tmpdir.py + _pytest/unittest.py _pytest/unraisableexception.py _pytest/warning_types.py - _pytest/warnings.py + _pytest/warnings.py pytest/__init__.py pytest/__main__.py pytest/collect.py -) - +) + RESOURCE_FILES( PREFIX contrib/python/pytest/py3/ .dist-info/METADATA @@ -105,4 +105,4 @@ RESOURCE_FILES( pytest/py.typed ) -END() +END() |