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 | |
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')
139 files changed, 31106 insertions, 31106 deletions
diff --git a/contrib/python/pluggy/py2/pluggy/__init__.py b/contrib/python/pluggy/py2/pluggy/__init__.py index da33974c59..fb4f991a61 100644 --- a/contrib/python/pluggy/py2/pluggy/__init__.py +++ b/contrib/python/pluggy/py2/pluggy/__init__.py @@ -1,18 +1,18 @@ -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__ = [ - "PluginManager", - "PluginValidationError", - "HookCallError", - "HookspecMarker", - "HookimplMarker", -] - -from .manager import PluginManager, PluginValidationError -from .callers import HookCallError -from .hooks import HookspecMarker, HookimplMarker +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__ = [ + "PluginManager", + "PluginValidationError", + "HookCallError", + "HookspecMarker", + "HookimplMarker", +] + +from .manager import PluginManager, PluginValidationError +from .callers import HookCallError +from .hooks import HookspecMarker, HookimplMarker diff --git a/contrib/python/pluggy/py2/pluggy/_tracing.py b/contrib/python/pluggy/py2/pluggy/_tracing.py index 2a80152a92..8b9715f026 100644 --- a/contrib/python/pluggy/py2/pluggy/_tracing.py +++ b/contrib/python/pluggy/py2/pluggy/_tracing.py @@ -1,62 +1,62 @@ -""" -Tracing utils -""" - - -class TagTracer(object): - def __init__(self): +""" +Tracing utils +""" + + +class TagTracer(object): + def __init__(self): self._tags2proc = {} self._writer = None - self.indent = 0 - - def get(self, name): - return TagTracerSub(self, (name,)) - + self.indent = 0 + + def get(self, name): + return TagTracerSub(self, (name,)) + def _format_message(self, tags, args): - if isinstance(args[-1], dict): - extra = args[-1] - args = args[:-1] - else: - extra = {} - - content = " ".join(map(str, args)) - indent = " " * self.indent - - lines = ["%s%s [%s]\n" % (indent, content, ":".join(tags))] - - for name, value in extra.items(): - lines.append("%s %s: %s\n" % (indent, name, value)) - + if isinstance(args[-1], dict): + extra = args[-1] + args = args[:-1] + else: + extra = {} + + content = " ".join(map(str, args)) + indent = " " * self.indent + + lines = ["%s%s [%s]\n" % (indent, content, ":".join(tags))] + + for name, value in extra.items(): + lines.append("%s %s: %s\n" % (indent, name, value)) + return "".join(lines) def _processmessage(self, tags, args): if self._writer is not None and args: self._writer(self._format_message(tags, args)) - try: + try: processor = self._tags2proc[tags] - except KeyError: - pass + except KeyError: + pass else: processor(tags, args) - - def setwriter(self, writer): + + def setwriter(self, writer): self._writer = writer - - def setprocessor(self, tags, processor): - if isinstance(tags, str): - tags = tuple(tags.split(":")) - else: - assert isinstance(tags, tuple) + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + assert isinstance(tags, tuple) self._tags2proc[tags] = processor - - -class TagTracerSub(object): - def __init__(self, root, tags): - self.root = root - self.tags = tags - - def __call__(self, *args): + + +class TagTracerSub(object): + def __init__(self, root, tags): + self.root = root + self.tags = tags + + def __call__(self, *args): self.root._processmessage(self.tags, args) - - def get(self, name): - return self.__class__(self.root, self.tags + (name,)) + + def get(self, name): + return self.__class__(self.root, self.tags + (name,)) diff --git a/contrib/python/pluggy/py2/pluggy/_version.py b/contrib/python/pluggy/py2/pluggy/_version.py index 41ad9f5ba5..2ba90cb83e 100644 --- a/contrib/python/pluggy/py2/pluggy/_version.py +++ b/contrib/python/pluggy/py2/pluggy/_version.py @@ -1,4 +1,4 @@ -# 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 = '0.13.1' diff --git a/contrib/python/pluggy/py2/pluggy/callers.py b/contrib/python/pluggy/py2/pluggy/callers.py index ff0b5e8fd2..e7ea464b0d 100644 --- a/contrib/python/pluggy/py2/pluggy/callers.py +++ b/contrib/python/pluggy/py2/pluggy/callers.py @@ -1,208 +1,208 @@ -""" -Call loop machinery -""" -import sys -import warnings - -_py3 = sys.version_info > (3, 0) - - -if not _py3: - exec( - """ -def _reraise(cls, val, tb): - raise cls, val, tb -""" - ) - - -def _raise_wrapfail(wrap_controller, msg): - co = wrap_controller.gi_code - raise RuntimeError( - "wrap_controller at %r %s:%d %s" - % (co.co_name, co.co_filename, co.co_firstlineno, msg) - ) - - -class HookCallError(Exception): - """ Hook was called wrongly. """ - - -class _Result(object): - def __init__(self, result, excinfo): - self._result = result - self._excinfo = excinfo - - @property - def excinfo(self): - return self._excinfo - - @property - def result(self): - """Get the result(s) for this hook call (DEPRECATED in favor of ``get_result()``).""" - msg = "Use get_result() which forces correct exception handling" - warnings.warn(DeprecationWarning(msg), stacklevel=2) - return self._result - - @classmethod - def from_call(cls, func): - __tracebackhide__ = True - result = excinfo = None - try: - result = func() - except BaseException: - excinfo = sys.exc_info() - - return cls(result, excinfo) - - def force_result(self, result): - """Force the result(s) to ``result``. - - If the hook was marked as a ``firstresult`` a single value should - be set otherwise set a (modified) list of results. Any exceptions - found during invocation will be deleted. - """ - self._result = result - self._excinfo = None - - def get_result(self): - """Get the result(s) for this hook call. - - If the hook was marked as a ``firstresult`` only a single value - will be returned otherwise a list of results. - """ - __tracebackhide__ = True - if self._excinfo is None: - return self._result - else: - ex = self._excinfo - if _py3: - raise ex[1].with_traceback(ex[2]) - _reraise(*ex) # noqa - - -def _wrapped_call(wrap_controller, func): - """ Wrap calling to a function with a generator which needs to yield - exactly once. The yield point will trigger calling the wrapped function - and return its ``_Result`` to the yield point. The generator then needs - to finish (raise StopIteration) in order for the wrapped call to complete. - """ - try: - next(wrap_controller) # first yield - except StopIteration: - _raise_wrapfail(wrap_controller, "did not yield") - call_outcome = _Result.from_call(func) - try: - wrap_controller.send(call_outcome) - _raise_wrapfail(wrap_controller, "has second yield") - except StopIteration: - pass - return call_outcome.get_result() - - -class _LegacyMultiCall(object): - """ execute a call into multiple python functions/methods. """ - - # XXX note that the __multicall__ argument is supported only - # for pytest compatibility reasons. It was never officially - # supported there and is explicitely deprecated since 2.8 - # so we can remove it soon, allowing to avoid the below recursion - # in execute() and simplify/speed up the execute loop. - - def __init__(self, hook_impls, kwargs, firstresult=False): - self.hook_impls = hook_impls - self.caller_kwargs = kwargs # come from _HookCaller.__call__() - self.caller_kwargs["__multicall__"] = self - self.firstresult = firstresult - - def execute(self): - caller_kwargs = self.caller_kwargs - self.results = results = [] - firstresult = self.firstresult - - while self.hook_impls: - hook_impl = self.hook_impls.pop() - try: - args = [caller_kwargs[argname] for argname in hook_impl.argnames] - except KeyError: - for argname in hook_impl.argnames: - if argname not in caller_kwargs: - raise HookCallError( - "hook call must provide argument %r" % (argname,) - ) - if hook_impl.hookwrapper: - return _wrapped_call(hook_impl.function(*args), self.execute) - res = hook_impl.function(*args) - if res is not None: - if firstresult: - return res - results.append(res) - - if not firstresult: - return results - - def __repr__(self): - status = "%d meths" % (len(self.hook_impls),) - if hasattr(self, "results"): - status = ("%d results, " % len(self.results)) + status - return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) - - -def _legacymulticall(hook_impls, caller_kwargs, firstresult=False): - return _LegacyMultiCall( - hook_impls, caller_kwargs, firstresult=firstresult - ).execute() - - -def _multicall(hook_impls, caller_kwargs, firstresult=False): - """Execute a call into multiple python functions/methods and return the - result(s). - - ``caller_kwargs`` comes from _HookCaller.__call__(). - """ - __tracebackhide__ = True - results = [] - excinfo = None - try: # run impl and wrapper setup functions in a loop - teardowns = [] - try: - for hook_impl in reversed(hook_impls): - try: - args = [caller_kwargs[argname] for argname in hook_impl.argnames] - except KeyError: - for argname in hook_impl.argnames: - if argname not in caller_kwargs: - raise HookCallError( - "hook call must provide argument %r" % (argname,) - ) - - if hook_impl.hookwrapper: - try: - gen = hook_impl.function(*args) - next(gen) # first yield - teardowns.append(gen) - except StopIteration: - _raise_wrapfail(gen, "did not yield") - else: - res = hook_impl.function(*args) - if res is not None: - results.append(res) - if firstresult: # halt further impl calls - break - except BaseException: - excinfo = sys.exc_info() - finally: - if firstresult: # first result hooks return a single value - outcome = _Result(results[0] if results else None, excinfo) - else: - outcome = _Result(results, excinfo) - - # run all wrapper post-yield blocks - for gen in reversed(teardowns): - try: - gen.send(outcome) - _raise_wrapfail(gen, "has second yield") - except StopIteration: - pass - - return outcome.get_result() +""" +Call loop machinery +""" +import sys +import warnings + +_py3 = sys.version_info > (3, 0) + + +if not _py3: + exec( + """ +def _reraise(cls, val, tb): + raise cls, val, tb +""" + ) + + +def _raise_wrapfail(wrap_controller, msg): + co = wrap_controller.gi_code + raise RuntimeError( + "wrap_controller at %r %s:%d %s" + % (co.co_name, co.co_filename, co.co_firstlineno, msg) + ) + + +class HookCallError(Exception): + """ Hook was called wrongly. """ + + +class _Result(object): + def __init__(self, result, excinfo): + self._result = result + self._excinfo = excinfo + + @property + def excinfo(self): + return self._excinfo + + @property + def result(self): + """Get the result(s) for this hook call (DEPRECATED in favor of ``get_result()``).""" + msg = "Use get_result() which forces correct exception handling" + warnings.warn(DeprecationWarning(msg), stacklevel=2) + return self._result + + @classmethod + def from_call(cls, func): + __tracebackhide__ = True + result = excinfo = None + try: + result = func() + except BaseException: + excinfo = sys.exc_info() + + return cls(result, excinfo) + + def force_result(self, result): + """Force the result(s) to ``result``. + + If the hook was marked as a ``firstresult`` a single value should + be set otherwise set a (modified) list of results. Any exceptions + found during invocation will be deleted. + """ + self._result = result + self._excinfo = None + + def get_result(self): + """Get the result(s) for this hook call. + + If the hook was marked as a ``firstresult`` only a single value + will be returned otherwise a list of results. + """ + __tracebackhide__ = True + if self._excinfo is None: + return self._result + else: + ex = self._excinfo + if _py3: + raise ex[1].with_traceback(ex[2]) + _reraise(*ex) # noqa + + +def _wrapped_call(wrap_controller, func): + """ Wrap calling to a function with a generator which needs to yield + exactly once. The yield point will trigger calling the wrapped function + and return its ``_Result`` to the yield point. The generator then needs + to finish (raise StopIteration) in order for the wrapped call to complete. + """ + try: + next(wrap_controller) # first yield + except StopIteration: + _raise_wrapfail(wrap_controller, "did not yield") + call_outcome = _Result.from_call(func) + try: + wrap_controller.send(call_outcome) + _raise_wrapfail(wrap_controller, "has second yield") + except StopIteration: + pass + return call_outcome.get_result() + + +class _LegacyMultiCall(object): + """ execute a call into multiple python functions/methods. """ + + # XXX note that the __multicall__ argument is supported only + # for pytest compatibility reasons. It was never officially + # supported there and is explicitely deprecated since 2.8 + # so we can remove it soon, allowing to avoid the below recursion + # in execute() and simplify/speed up the execute loop. + + def __init__(self, hook_impls, kwargs, firstresult=False): + self.hook_impls = hook_impls + self.caller_kwargs = kwargs # come from _HookCaller.__call__() + self.caller_kwargs["__multicall__"] = self + self.firstresult = firstresult + + def execute(self): + caller_kwargs = self.caller_kwargs + self.results = results = [] + firstresult = self.firstresult + + while self.hook_impls: + hook_impl = self.hook_impls.pop() + try: + args = [caller_kwargs[argname] for argname in hook_impl.argnames] + except KeyError: + for argname in hook_impl.argnames: + if argname not in caller_kwargs: + raise HookCallError( + "hook call must provide argument %r" % (argname,) + ) + if hook_impl.hookwrapper: + return _wrapped_call(hook_impl.function(*args), self.execute) + res = hook_impl.function(*args) + if res is not None: + if firstresult: + return res + results.append(res) + + if not firstresult: + return results + + def __repr__(self): + status = "%d meths" % (len(self.hook_impls),) + if hasattr(self, "results"): + status = ("%d results, " % len(self.results)) + status + return "<_MultiCall %s, kwargs=%r>" % (status, self.caller_kwargs) + + +def _legacymulticall(hook_impls, caller_kwargs, firstresult=False): + return _LegacyMultiCall( + hook_impls, caller_kwargs, firstresult=firstresult + ).execute() + + +def _multicall(hook_impls, caller_kwargs, firstresult=False): + """Execute a call into multiple python functions/methods and return the + result(s). + + ``caller_kwargs`` comes from _HookCaller.__call__(). + """ + __tracebackhide__ = True + results = [] + excinfo = None + try: # run impl and wrapper setup functions in a loop + teardowns = [] + try: + for hook_impl in reversed(hook_impls): + try: + args = [caller_kwargs[argname] for argname in hook_impl.argnames] + except KeyError: + for argname in hook_impl.argnames: + if argname not in caller_kwargs: + raise HookCallError( + "hook call must provide argument %r" % (argname,) + ) + + if hook_impl.hookwrapper: + try: + gen = hook_impl.function(*args) + next(gen) # first yield + teardowns.append(gen) + except StopIteration: + _raise_wrapfail(gen, "did not yield") + else: + res = hook_impl.function(*args) + if res is not None: + results.append(res) + if firstresult: # halt further impl calls + break + except BaseException: + excinfo = sys.exc_info() + finally: + if firstresult: # first result hooks return a single value + outcome = _Result(results[0] if results else None, excinfo) + else: + outcome = _Result(results, excinfo) + + # run all wrapper post-yield blocks + for gen in reversed(teardowns): + try: + gen.send(outcome) + _raise_wrapfail(gen, "has second yield") + except StopIteration: + pass + + return outcome.get_result() diff --git a/contrib/python/pluggy/py2/pluggy/hooks.py b/contrib/python/pluggy/py2/pluggy/hooks.py index 66133d063e..0a1c287198 100644 --- a/contrib/python/pluggy/py2/pluggy/hooks.py +++ b/contrib/python/pluggy/py2/pluggy/hooks.py @@ -1,359 +1,359 @@ -""" -Internal hook annotation, representation and calling machinery. -""" -import inspect +""" +Internal hook annotation, representation and calling machinery. +""" +import inspect import sys -import warnings -from .callers import _legacymulticall, _multicall - - -class HookspecMarker(object): - """ Decorator helper class for marking functions as hook specifications. - - You can instantiate it with a project_name to get a decorator. +import warnings +from .callers import _legacymulticall, _multicall + + +class HookspecMarker(object): + """ Decorator helper class for marking functions as hook specifications. + + You can instantiate it with a project_name to get a decorator. Calling :py:meth:`.PluginManager.add_hookspecs` later will discover all marked functions if the :py:class:`.PluginManager` uses the same project_name. - """ - - def __init__(self, project_name): - self.project_name = project_name - - def __call__( - self, function=None, firstresult=False, historic=False, warn_on_impl=None - ): - """ if passed a function, directly sets attributes on the function + """ + + def __init__(self, project_name): + self.project_name = project_name + + def __call__( + self, function=None, firstresult=False, historic=False, warn_on_impl=None + ): + """ if passed a function, directly sets attributes on the function which will make it discoverable to :py:meth:`.PluginManager.add_hookspecs`. If passed no function, returns a decorator which can be applied to a function - later using the attributes supplied. - + later using the attributes supplied. + If ``firstresult`` is ``True`` the 1:N hook call (N being the number of registered - hook implementation functions) will stop at I<=N when the I'th function + hook implementation functions) will stop at I<=N when the I'th function returns a non-``None`` result. - + If ``historic`` is ``True`` calls to a hook will be memorized and replayed - on later registered plugins. - - """ - - def setattr_hookspec_opts(func): - if historic and firstresult: - raise ValueError("cannot have a historic firstresult hook") - setattr( - func, - self.project_name + "_spec", - dict( - firstresult=firstresult, - historic=historic, - warn_on_impl=warn_on_impl, - ), - ) - return func - - if function is not None: - return setattr_hookspec_opts(function) - else: - return setattr_hookspec_opts - - -class HookimplMarker(object): - """ Decorator helper class for marking functions as hook implementations. - + on later registered plugins. + + """ + + def setattr_hookspec_opts(func): + if historic and firstresult: + raise ValueError("cannot have a historic firstresult hook") + setattr( + func, + self.project_name + "_spec", + dict( + firstresult=firstresult, + historic=historic, + warn_on_impl=warn_on_impl, + ), + ) + return func + + if function is not None: + return setattr_hookspec_opts(function) + else: + return setattr_hookspec_opts + + +class HookimplMarker(object): + """ Decorator helper class for marking functions as hook implementations. + You can instantiate with a ``project_name`` to get a decorator. Calling :py:meth:`.PluginManager.register` later will discover all marked functions if the :py:class:`.PluginManager` uses the same project_name. - """ - - def __init__(self, project_name): - self.project_name = project_name - - def __call__( - self, - function=None, - hookwrapper=False, - optionalhook=False, - tryfirst=False, - trylast=False, - ): - - """ if passed a function, directly sets attributes on the function + """ + + def __init__(self, project_name): + self.project_name = project_name + + def __call__( + self, + function=None, + hookwrapper=False, + optionalhook=False, + tryfirst=False, + trylast=False, + ): + + """ if passed a function, directly sets attributes on the function which will make it discoverable to :py:meth:`.PluginManager.register`. If passed no function, returns a decorator which can be applied to a function later using the attributes supplied. - + If ``optionalhook`` is ``True`` a missing matching hook specification will not result - in an error (by default it is an error if no matching spec is found). - + in an error (by default it is an error if no matching spec is found). + If ``tryfirst`` is ``True`` this hook implementation will run as early as possible in the chain of N hook implementations for a specification. - + If ``trylast`` is ``True`` this hook implementation will run as late as possible - in the chain of N hook implementations. - + in the chain of N hook implementations. + If ``hookwrapper`` is ``True`` the hook implementations needs to execute exactly one ``yield``. The code before the ``yield`` is run early before any non-hookwrapper function is run. The code after the ``yield`` is run after all non-hookwrapper function have run. The ``yield`` receives a :py:class:`.callers._Result` object representing the exception or result outcome of the inner calls (including other hookwrapper calls). - - """ - - def setattr_hookimpl_opts(func): - setattr( - func, - self.project_name + "_impl", - dict( - hookwrapper=hookwrapper, - optionalhook=optionalhook, - tryfirst=tryfirst, - trylast=trylast, - ), - ) - return func - - if function is None: - return setattr_hookimpl_opts - else: - return setattr_hookimpl_opts(function) - - -def normalize_hookimpl_opts(opts): - opts.setdefault("tryfirst", False) - opts.setdefault("trylast", False) - opts.setdefault("hookwrapper", False) - opts.setdefault("optionalhook", False) - - -if hasattr(inspect, "getfullargspec"): - - def _getargspec(func): - return inspect.getfullargspec(func) - - -else: - - def _getargspec(func): - return inspect.getargspec(func) - - + + """ + + def setattr_hookimpl_opts(func): + setattr( + func, + self.project_name + "_impl", + dict( + hookwrapper=hookwrapper, + optionalhook=optionalhook, + tryfirst=tryfirst, + trylast=trylast, + ), + ) + return func + + if function is None: + return setattr_hookimpl_opts + else: + return setattr_hookimpl_opts(function) + + +def normalize_hookimpl_opts(opts): + opts.setdefault("tryfirst", False) + opts.setdefault("trylast", False) + opts.setdefault("hookwrapper", False) + opts.setdefault("optionalhook", False) + + +if hasattr(inspect, "getfullargspec"): + + def _getargspec(func): + return inspect.getfullargspec(func) + + +else: + + def _getargspec(func): + return inspect.getargspec(func) + + _PYPY3 = hasattr(sys, "pypy_version_info") and sys.version_info.major == 3 -def varnames(func): - """Return tuple of positional and keywrord argument names for a function, - method, class or callable. - - In case of a class, its ``__init__`` method is considered. - For methods the ``self`` parameter is not included. - """ - cache = getattr(func, "__dict__", {}) - try: - return cache["_varnames"] - except KeyError: - pass - - if inspect.isclass(func): - try: - func = func.__init__ - except AttributeError: - return (), () - elif not inspect.isroutine(func): # callable object? - try: - func = getattr(func, "__call__", func) - except Exception: +def varnames(func): + """Return tuple of positional and keywrord argument names for a function, + method, class or callable. + + In case of a class, its ``__init__`` method is considered. + For methods the ``self`` parameter is not included. + """ + cache = getattr(func, "__dict__", {}) + try: + return cache["_varnames"] + except KeyError: + pass + + if inspect.isclass(func): + try: + func = func.__init__ + except AttributeError: + return (), () + elif not inspect.isroutine(func): # callable object? + try: + func = getattr(func, "__call__", func) + except Exception: return (), () - - try: # func MUST be a function or method here or we won't parse any args - spec = _getargspec(func) - except TypeError: - return (), () - - args, defaults = tuple(spec.args), spec.defaults - if defaults: - index = -len(defaults) + + try: # func MUST be a function or method here or we won't parse any args + spec = _getargspec(func) + except TypeError: + return (), () + + args, defaults = tuple(spec.args), spec.defaults + if defaults: + index = -len(defaults) args, kwargs = args[:index], tuple(args[index:]) - else: + else: kwargs = () - - # strip any implicit instance arg + + # strip any implicit instance arg # pypy3 uses "obj" instead of "self" for default dunder methods implicit_names = ("self",) if not _PYPY3 else ("self", "obj") - if args: - if inspect.ismethod(func) or ( + if args: + if inspect.ismethod(func) or ( "." in getattr(func, "__qualname__", ()) and args[0] in implicit_names - ): - args = args[1:] - - try: + ): + args = args[1:] + + try: cache["_varnames"] = args, kwargs - except TypeError: - pass + except TypeError: + pass return args, kwargs - - -class _HookRelay(object): - """ hook holder object for performing 1:N hook calls where N is the number - of registered plugins. - - """ - - -class _HookCaller(object): - def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None): - self.name = name - self._wrappers = [] - self._nonwrappers = [] - self._hookexec = hook_execute - self.argnames = None - self.kwargnames = None - self.multicall = _multicall - self.spec = None - if specmodule_or_class is not None: - assert spec_opts is not None - self.set_specification(specmodule_or_class, spec_opts) - - def has_spec(self): - return self.spec is not None - - def set_specification(self, specmodule_or_class, spec_opts): - assert not self.has_spec() - self.spec = HookSpec(specmodule_or_class, self.name, spec_opts) - if spec_opts.get("historic"): - self._call_history = [] - - def is_historic(self): - return hasattr(self, "_call_history") - - def _remove_plugin(self, plugin): - def remove(wrappers): - for i, method in enumerate(wrappers): - if method.plugin == plugin: - del wrappers[i] - return True - - if remove(self._wrappers) is None: - if remove(self._nonwrappers) is None: - raise ValueError("plugin %r not found" % (plugin,)) - - def get_hookimpls(self): - # Order is important for _hookexec - return self._nonwrappers + self._wrappers - - def _add_hookimpl(self, hookimpl): - """Add an implementation to the callback chain. - """ - if hookimpl.hookwrapper: - methods = self._wrappers - else: - methods = self._nonwrappers - - if hookimpl.trylast: - methods.insert(0, hookimpl) - elif hookimpl.tryfirst: - methods.append(hookimpl) - else: - # find last non-tryfirst method - i = len(methods) - 1 - while i >= 0 and methods[i].tryfirst: - i -= 1 - methods.insert(i + 1, hookimpl) - - if "__multicall__" in hookimpl.argnames: - warnings.warn( - "Support for __multicall__ is now deprecated and will be" - "removed in an upcoming release.", - DeprecationWarning, - ) - self.multicall = _legacymulticall - - def __repr__(self): - return "<_HookCaller %r>" % (self.name,) - - def __call__(self, *args, **kwargs): - if args: - raise TypeError("hook calling supports only keyword arguments") - assert not self.is_historic() - if self.spec and self.spec.argnames: - notincall = ( - set(self.spec.argnames) - set(["__multicall__"]) - set(kwargs.keys()) - ) - if notincall: - warnings.warn( - "Argument(s) {} which are declared in the hookspec " - "can not be found in this hook call".format(tuple(notincall)), - stacklevel=2, - ) - return self._hookexec(self, self.get_hookimpls(), kwargs) - - def call_historic(self, result_callback=None, kwargs=None, proc=None): - """Call the hook with given ``kwargs`` for all registered plugins and - for all plugins which will be registered afterwards. - - If ``result_callback`` is not ``None`` it will be called for for each + + +class _HookRelay(object): + """ hook holder object for performing 1:N hook calls where N is the number + of registered plugins. + + """ + + +class _HookCaller(object): + def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None): + self.name = name + self._wrappers = [] + self._nonwrappers = [] + self._hookexec = hook_execute + self.argnames = None + self.kwargnames = None + self.multicall = _multicall + self.spec = None + if specmodule_or_class is not None: + assert spec_opts is not None + self.set_specification(specmodule_or_class, spec_opts) + + def has_spec(self): + return self.spec is not None + + def set_specification(self, specmodule_or_class, spec_opts): + assert not self.has_spec() + self.spec = HookSpec(specmodule_or_class, self.name, spec_opts) + if spec_opts.get("historic"): + self._call_history = [] + + def is_historic(self): + return hasattr(self, "_call_history") + + def _remove_plugin(self, plugin): + def remove(wrappers): + for i, method in enumerate(wrappers): + if method.plugin == plugin: + del wrappers[i] + return True + + if remove(self._wrappers) is None: + if remove(self._nonwrappers) is None: + raise ValueError("plugin %r not found" % (plugin,)) + + def get_hookimpls(self): + # Order is important for _hookexec + return self._nonwrappers + self._wrappers + + def _add_hookimpl(self, hookimpl): + """Add an implementation to the callback chain. + """ + if hookimpl.hookwrapper: + methods = self._wrappers + else: + methods = self._nonwrappers + + if hookimpl.trylast: + methods.insert(0, hookimpl) + elif hookimpl.tryfirst: + methods.append(hookimpl) + else: + # find last non-tryfirst method + i = len(methods) - 1 + while i >= 0 and methods[i].tryfirst: + i -= 1 + methods.insert(i + 1, hookimpl) + + if "__multicall__" in hookimpl.argnames: + warnings.warn( + "Support for __multicall__ is now deprecated and will be" + "removed in an upcoming release.", + DeprecationWarning, + ) + self.multicall = _legacymulticall + + def __repr__(self): + return "<_HookCaller %r>" % (self.name,) + + def __call__(self, *args, **kwargs): + if args: + raise TypeError("hook calling supports only keyword arguments") + assert not self.is_historic() + if self.spec and self.spec.argnames: + notincall = ( + set(self.spec.argnames) - set(["__multicall__"]) - set(kwargs.keys()) + ) + if notincall: + warnings.warn( + "Argument(s) {} which are declared in the hookspec " + "can not be found in this hook call".format(tuple(notincall)), + stacklevel=2, + ) + return self._hookexec(self, self.get_hookimpls(), kwargs) + + def call_historic(self, result_callback=None, kwargs=None, proc=None): + """Call the hook with given ``kwargs`` for all registered plugins and + for all plugins which will be registered afterwards. + + If ``result_callback`` is not ``None`` it will be called for for each non-``None`` result obtained from a hook implementation. - - .. note:: - The ``proc`` argument is now deprecated. - """ - if proc is not None: - warnings.warn( - "Support for `proc` argument is now deprecated and will be" - "removed in an upcoming release.", - DeprecationWarning, - ) - result_callback = proc - - self._call_history.append((kwargs or {}, result_callback)) - # historizing hooks don't return results - res = self._hookexec(self, self.get_hookimpls(), kwargs) - if result_callback is None: - return - # XXX: remember firstresult isn't compat with historic - for x in res or []: - result_callback(x) - - def call_extra(self, methods, kwargs): - """ Call the hook with some additional temporarily participating + + .. note:: + The ``proc`` argument is now deprecated. + """ + if proc is not None: + warnings.warn( + "Support for `proc` argument is now deprecated and will be" + "removed in an upcoming release.", + DeprecationWarning, + ) + result_callback = proc + + self._call_history.append((kwargs or {}, result_callback)) + # historizing hooks don't return results + res = self._hookexec(self, self.get_hookimpls(), kwargs) + if result_callback is None: + return + # XXX: remember firstresult isn't compat with historic + for x in res or []: + result_callback(x) + + def call_extra(self, methods, kwargs): + """ Call the hook with some additional temporarily participating methods using the specified ``kwargs`` as call parameters. """ - old = list(self._nonwrappers), list(self._wrappers) - for method in methods: - opts = dict(hookwrapper=False, trylast=False, tryfirst=False) - hookimpl = HookImpl(None, "<temp>", method, opts) - self._add_hookimpl(hookimpl) - try: - return self(**kwargs) - finally: - self._nonwrappers, self._wrappers = old - - def _maybe_apply_history(self, method): - """Apply call history to a new hookimpl if it is marked as historic. - """ - if self.is_historic(): - for kwargs, result_callback in self._call_history: - res = self._hookexec(self, [method], kwargs) - if res and result_callback is not None: - result_callback(res[0]) - - -class HookImpl(object): - def __init__(self, plugin, plugin_name, function, hook_impl_opts): - self.function = function - self.argnames, self.kwargnames = varnames(self.function) - self.plugin = plugin - self.opts = hook_impl_opts - self.plugin_name = plugin_name - self.__dict__.update(hook_impl_opts) - - def __repr__(self): - return "<HookImpl plugin_name=%r, plugin=%r>" % (self.plugin_name, self.plugin) - - -class HookSpec(object): - def __init__(self, namespace, name, opts): - self.namespace = namespace - self.function = function = getattr(namespace, name) - self.name = name - self.argnames, self.kwargnames = varnames(function) - self.opts = opts - self.argnames = ["__multicall__"] + list(self.argnames) - self.warn_on_impl = opts.get("warn_on_impl") + old = list(self._nonwrappers), list(self._wrappers) + for method in methods: + opts = dict(hookwrapper=False, trylast=False, tryfirst=False) + hookimpl = HookImpl(None, "<temp>", method, opts) + self._add_hookimpl(hookimpl) + try: + return self(**kwargs) + finally: + self._nonwrappers, self._wrappers = old + + def _maybe_apply_history(self, method): + """Apply call history to a new hookimpl if it is marked as historic. + """ + if self.is_historic(): + for kwargs, result_callback in self._call_history: + res = self._hookexec(self, [method], kwargs) + if res and result_callback is not None: + result_callback(res[0]) + + +class HookImpl(object): + def __init__(self, plugin, plugin_name, function, hook_impl_opts): + self.function = function + self.argnames, self.kwargnames = varnames(self.function) + self.plugin = plugin + self.opts = hook_impl_opts + self.plugin_name = plugin_name + self.__dict__.update(hook_impl_opts) + + def __repr__(self): + return "<HookImpl plugin_name=%r, plugin=%r>" % (self.plugin_name, self.plugin) + + +class HookSpec(object): + def __init__(self, namespace, name, opts): + self.namespace = namespace + self.function = function = getattr(namespace, name) + self.name = name + self.argnames, self.kwargnames = varnames(function) + self.opts = opts + self.argnames = ["__multicall__"] + list(self.argnames) + self.warn_on_impl = opts.get("warn_on_impl") diff --git a/contrib/python/pluggy/py2/pluggy/manager.py b/contrib/python/pluggy/py2/pluggy/manager.py index 2472482318..07b42cba2d 100644 --- a/contrib/python/pluggy/py2/pluggy/manager.py +++ b/contrib/python/pluggy/py2/pluggy/manager.py @@ -1,37 +1,37 @@ -import inspect +import inspect import sys -from . import _tracing +from . import _tracing from .callers import _Result -from .hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts -import warnings - +from .hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts +import warnings + if sys.version_info >= (3, 8): from importlib import metadata as importlib_metadata else: import importlib_metadata - - -def _warn_for_function(warning, function): - warnings.warn_explicit( - warning, - type(warning), - lineno=function.__code__.co_firstlineno, - filename=function.__code__.co_filename, - ) - - -class PluginValidationError(Exception): - """ plugin failed validation. - - :param object plugin: the plugin which failed validation, - may be a module or an arbitrary object. - """ - - def __init__(self, plugin, message): - self.plugin = plugin - super(Exception, self).__init__(message) - - + + +def _warn_for_function(warning, function): + warnings.warn_explicit( + warning, + type(warning), + lineno=function.__code__.co_firstlineno, + filename=function.__code__.co_filename, + ) + + +class PluginValidationError(Exception): + """ plugin failed validation. + + :param object plugin: the plugin which failed validation, + may be a module or an arbitrary object. + """ + + def __init__(self, plugin, message): + self.plugin = plugin + super(Exception, self).__init__(message) + + class DistFacade(object): """Emulate a pkg_resources Distribution""" @@ -49,234 +49,234 @@ class DistFacade(object): return sorted(dir(self._dist) + ["_dist", "project_name"]) -class PluginManager(object): +class PluginManager(object): """ Core :py:class:`.PluginManager` class which manages registration - of plugin objects and 1:N hook calling. - + of plugin objects and 1:N hook calling. + You can register new hooks by calling :py:meth:`add_hookspecs(module_or_class) <.PluginManager.add_hookspecs>`. - You can register plugin objects (which contain hooks) by calling + You can register plugin objects (which contain hooks) by calling :py:meth:`register(plugin) <.PluginManager.register>`. The :py:class:`.PluginManager` is initialized with a prefix that is searched for in the names of the dict of registered plugin objects. - + For debugging purposes you can call :py:meth:`.PluginManager.enable_tracing` - which will subsequently send debug information to the trace helper. - """ - - def __init__(self, project_name, implprefix=None): - """If ``implprefix`` is given implementation functions + which will subsequently send debug information to the trace helper. + """ + + def __init__(self, project_name, implprefix=None): + """If ``implprefix`` is given implementation functions will be recognized if their name matches the ``implprefix``. """ - self.project_name = project_name - self._name2plugin = {} - self._plugin2hookcallers = {} - self._plugin_distinfo = [] - self.trace = _tracing.TagTracer().get("pluginmanage") + self.project_name = project_name + self._name2plugin = {} + self._plugin2hookcallers = {} + self._plugin_distinfo = [] + self.trace = _tracing.TagTracer().get("pluginmanage") self.hook = _HookRelay() - if implprefix is not None: - warnings.warn( - "Support for the `implprefix` arg is now deprecated and will " - "be removed in an upcoming release. Please use HookimplMarker.", - DeprecationWarning, + if implprefix is not None: + warnings.warn( + "Support for the `implprefix` arg is now deprecated and will " + "be removed in an upcoming release. Please use HookimplMarker.", + DeprecationWarning, stacklevel=2, - ) - self._implprefix = implprefix - self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall( - methods, - kwargs, - firstresult=hook.spec.opts.get("firstresult") if hook.spec else False, - ) - - def _hookexec(self, hook, methods, kwargs): - # called from all hookcaller instances. - # enable_tracing will set its own wrapping function at self._inner_hookexec - return self._inner_hookexec(hook, methods, kwargs) - - def register(self, plugin, name=None): + ) + self._implprefix = implprefix + self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall( + methods, + kwargs, + firstresult=hook.spec.opts.get("firstresult") if hook.spec else False, + ) + + def _hookexec(self, hook, methods, kwargs): + # called from all hookcaller instances. + # enable_tracing will set its own wrapping function at self._inner_hookexec + return self._inner_hookexec(hook, methods, kwargs) + + def register(self, plugin, name=None): """ Register a plugin and return its canonical name or ``None`` if the name is blocked from registering. Raise a :py:class:`ValueError` if the plugin is already registered. """ - plugin_name = name or self.get_canonical_name(plugin) - - if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers: - if self._name2plugin.get(plugin_name, -1) is None: - return # blocked plugin, return None to indicate no registration - raise ValueError( - "Plugin already registered: %s=%s\n%s" - % (plugin_name, plugin, self._name2plugin) - ) - - # XXX if an error happens we should make sure no state has been - # changed at point of return - self._name2plugin[plugin_name] = plugin - - # register matching hook implementations of the plugin - self._plugin2hookcallers[plugin] = hookcallers = [] - for name in dir(plugin): - hookimpl_opts = self.parse_hookimpl_opts(plugin, name) - if hookimpl_opts is not None: - normalize_hookimpl_opts(hookimpl_opts) - method = getattr(plugin, name) - hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) - hook = getattr(self.hook, name, None) - if hook is None: - hook = _HookCaller(name, self._hookexec) - setattr(self.hook, name, hook) - elif hook.has_spec(): - self._verify_hook(hook, hookimpl) - hook._maybe_apply_history(hookimpl) - hook._add_hookimpl(hookimpl) - hookcallers.append(hook) - return plugin_name - - def parse_hookimpl_opts(self, plugin, name): - method = getattr(plugin, name) - if not inspect.isroutine(method): - return - try: - res = getattr(method, self.project_name + "_impl", None) - except Exception: - res = {} - if res is not None and not isinstance(res, dict): - # false positive - res = None - # TODO: remove when we drop implprefix in 1.0 - elif res is None and self._implprefix and name.startswith(self._implprefix): - _warn_for_function( - DeprecationWarning( - "The `implprefix` system is deprecated please decorate " - "this function using an instance of HookimplMarker." - ), - method, - ) - res = {} - return res - - def unregister(self, plugin=None, name=None): - """ unregister a plugin object and all its contained hook implementations - from internal data structures. """ - if name is None: - assert plugin is not None, "one of name or plugin needs to be specified" - name = self.get_name(plugin) - - if plugin is None: - plugin = self.get_plugin(name) - - # if self._name2plugin[name] == None registration was blocked: ignore - if self._name2plugin.get(name): - del self._name2plugin[name] - - for hookcaller in self._plugin2hookcallers.pop(plugin, []): - hookcaller._remove_plugin(plugin) - - return plugin - - def set_blocked(self, name): - """ block registrations of the given name, unregister if already registered. """ - self.unregister(name=name) - self._name2plugin[name] = None - - def is_blocked(self, name): + plugin_name = name or self.get_canonical_name(plugin) + + if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers: + if self._name2plugin.get(plugin_name, -1) is None: + return # blocked plugin, return None to indicate no registration + raise ValueError( + "Plugin already registered: %s=%s\n%s" + % (plugin_name, plugin, self._name2plugin) + ) + + # XXX if an error happens we should make sure no state has been + # changed at point of return + self._name2plugin[plugin_name] = plugin + + # register matching hook implementations of the plugin + self._plugin2hookcallers[plugin] = hookcallers = [] + for name in dir(plugin): + hookimpl_opts = self.parse_hookimpl_opts(plugin, name) + if hookimpl_opts is not None: + normalize_hookimpl_opts(hookimpl_opts) + method = getattr(plugin, name) + hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts) + hook = getattr(self.hook, name, None) + if hook is None: + hook = _HookCaller(name, self._hookexec) + setattr(self.hook, name, hook) + elif hook.has_spec(): + self._verify_hook(hook, hookimpl) + hook._maybe_apply_history(hookimpl) + hook._add_hookimpl(hookimpl) + hookcallers.append(hook) + return plugin_name + + def parse_hookimpl_opts(self, plugin, name): + method = getattr(plugin, name) + if not inspect.isroutine(method): + return + try: + res = getattr(method, self.project_name + "_impl", None) + except Exception: + res = {} + if res is not None and not isinstance(res, dict): + # false positive + res = None + # TODO: remove when we drop implprefix in 1.0 + elif res is None and self._implprefix and name.startswith(self._implprefix): + _warn_for_function( + DeprecationWarning( + "The `implprefix` system is deprecated please decorate " + "this function using an instance of HookimplMarker." + ), + method, + ) + res = {} + return res + + def unregister(self, plugin=None, name=None): + """ unregister a plugin object and all its contained hook implementations + from internal data structures. """ + if name is None: + assert plugin is not None, "one of name or plugin needs to be specified" + name = self.get_name(plugin) + + if plugin is None: + plugin = self.get_plugin(name) + + # if self._name2plugin[name] == None registration was blocked: ignore + if self._name2plugin.get(name): + del self._name2plugin[name] + + for hookcaller in self._plugin2hookcallers.pop(plugin, []): + hookcaller._remove_plugin(plugin) + + return plugin + + def set_blocked(self, name): + """ block registrations of the given name, unregister if already registered. """ + self.unregister(name=name) + self._name2plugin[name] = None + + def is_blocked(self, name): """ return ``True`` if the given plugin name is blocked. """ - return name in self._name2plugin and self._name2plugin[name] is None - - def add_hookspecs(self, module_or_class): + return name in self._name2plugin and self._name2plugin[name] is None + + def add_hookspecs(self, module_or_class): """ add new hook specifications defined in the given ``module_or_class``. - Functions are recognized if they have been decorated accordingly. """ - names = [] - for name in dir(module_or_class): - spec_opts = self.parse_hookspec_opts(module_or_class, name) - if spec_opts is not None: - hc = getattr(self.hook, name, None) - if hc is None: - hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts) - setattr(self.hook, name, hc) - else: - # plugins registered this hook without knowing the spec - hc.set_specification(module_or_class, spec_opts) - for hookfunction in hc.get_hookimpls(): - self._verify_hook(hc, hookfunction) - names.append(name) - - if not names: - raise ValueError( - "did not find any %r hooks in %r" % (self.project_name, module_or_class) - ) - - def parse_hookspec_opts(self, module_or_class, name): - method = getattr(module_or_class, name) - return getattr(method, self.project_name + "_spec", None) - - def get_plugins(self): - """ return the set of registered plugins. """ - return set(self._plugin2hookcallers) - - def is_registered(self, plugin): + Functions are recognized if they have been decorated accordingly. """ + names = [] + for name in dir(module_or_class): + spec_opts = self.parse_hookspec_opts(module_or_class, name) + if spec_opts is not None: + hc = getattr(self.hook, name, None) + if hc is None: + hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts) + setattr(self.hook, name, hc) + else: + # plugins registered this hook without knowing the spec + hc.set_specification(module_or_class, spec_opts) + for hookfunction in hc.get_hookimpls(): + self._verify_hook(hc, hookfunction) + names.append(name) + + if not names: + raise ValueError( + "did not find any %r hooks in %r" % (self.project_name, module_or_class) + ) + + def parse_hookspec_opts(self, module_or_class, name): + method = getattr(module_or_class, name) + return getattr(method, self.project_name + "_spec", None) + + def get_plugins(self): + """ return the set of registered plugins. """ + return set(self._plugin2hookcallers) + + def is_registered(self, plugin): """ Return ``True`` if the plugin is already registered. """ - return plugin in self._plugin2hookcallers - - def get_canonical_name(self, plugin): - """ Return canonical name for a plugin object. Note that a plugin - may be registered under a different name which was specified + return plugin in self._plugin2hookcallers + + def get_canonical_name(self, plugin): + """ Return canonical name for a plugin object. Note that a plugin + may be registered under a different name which was specified by the caller of :py:meth:`register(plugin, name) <.PluginManager.register>`. To obtain the name of an registered plugin use :py:meth:`get_name(plugin) <.PluginManager.get_name>` instead.""" - return getattr(plugin, "__name__", None) or str(id(plugin)) - - def get_plugin(self, name): + return getattr(plugin, "__name__", None) or str(id(plugin)) + + def get_plugin(self, name): """ Return a plugin or ``None`` for the given name. """ - return self._name2plugin.get(name) - - def has_plugin(self, name): + return self._name2plugin.get(name) + + def has_plugin(self, name): """ Return ``True`` if a plugin with the given name is registered. """ - return self.get_plugin(name) is not None - - def get_name(self, plugin): + return self.get_plugin(name) is not None + + def get_name(self, plugin): """ Return name for registered plugin or ``None`` if not registered. """ - for name, val in self._name2plugin.items(): - if plugin == val: - return name - - def _verify_hook(self, hook, hookimpl): - if hook.is_historic() and hookimpl.hookwrapper: - raise PluginValidationError( - hookimpl.plugin, - "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" - % (hookimpl.plugin_name, hook.name), - ) - if hook.spec.warn_on_impl: - _warn_for_function(hook.spec.warn_on_impl, hookimpl.function) - # positional arg checking - notinspec = set(hookimpl.argnames) - set(hook.spec.argnames) - if notinspec: - raise PluginValidationError( - hookimpl.plugin, - "Plugin %r for hook %r\nhookimpl definition: %s\n" - "Argument(s) %s are declared in the hookimpl but " - "can not be found in the hookspec" - % ( - hookimpl.plugin_name, - hook.name, - _formatdef(hookimpl.function), - notinspec, - ), - ) - - def check_pending(self): - """ Verify that all hooks which have not been verified against + for name, val in self._name2plugin.items(): + if plugin == val: + return name + + def _verify_hook(self, hook, hookimpl): + if hook.is_historic() and hookimpl.hookwrapper: + raise PluginValidationError( + hookimpl.plugin, + "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" + % (hookimpl.plugin_name, hook.name), + ) + if hook.spec.warn_on_impl: + _warn_for_function(hook.spec.warn_on_impl, hookimpl.function) + # positional arg checking + notinspec = set(hookimpl.argnames) - set(hook.spec.argnames) + if notinspec: + raise PluginValidationError( + hookimpl.plugin, + "Plugin %r for hook %r\nhookimpl definition: %s\n" + "Argument(s) %s are declared in the hookimpl but " + "can not be found in the hookspec" + % ( + hookimpl.plugin_name, + hook.name, + _formatdef(hookimpl.function), + notinspec, + ), + ) + + def check_pending(self): + """ Verify that all hooks which have not been verified against a hook specification are optional, otherwise raise :py:class:`.PluginValidationError`.""" - for name in self.hook.__dict__: - if name[0] != "_": - hook = getattr(self.hook, name) - if not hook.has_spec(): - for hookimpl in hook.get_hookimpls(): - if not hookimpl.optionalhook: - raise PluginValidationError( - hookimpl.plugin, - "unknown hook %r in plugin %r" - % (name, hookimpl.plugin), - ) - + for name in self.hook.__dict__: + if name[0] != "_": + hook = getattr(self.hook, name) + if not hook.has_spec(): + for hookimpl in hook.get_hookimpls(): + if not hookimpl.optionalhook: + raise PluginValidationError( + hookimpl.plugin, + "unknown hook %r in plugin %r" + % (name, hookimpl.plugin), + ) + def load_setuptools_entrypoints(self, group, name=None): """ Load modules from querying the specified setuptools ``group``. @@ -296,40 +296,40 @@ class PluginManager(object): or self.is_blocked(ep.name) ): continue - plugin = ep.load() + plugin = ep.load() self.register(plugin, name=ep.name) self._plugin_distinfo.append((plugin, DistFacade(dist))) count += 1 return count - - def list_plugin_distinfo(self): - """ return list of distinfo/plugin tuples for all setuptools registered - plugins. """ - return list(self._plugin_distinfo) - - def list_name_plugin(self): - """ return list of name/plugin pairs. """ - return list(self._name2plugin.items()) - - def get_hookcallers(self, plugin): - """ get all hook callers for the specified plugin. """ - return self._plugin2hookcallers.get(plugin) - - def add_hookcall_monitoring(self, before, after): - """ add before/after tracing functions for all hooks - and return an undo function which, when called, - will remove the added tracers. - - ``before(hook_name, hook_impls, kwargs)`` will be called ahead - of all hook calls and receive a hookcaller instance, a list - of HookImpl instances and the keyword arguments for the hook call. - - ``after(outcome, hook_name, hook_impls, kwargs)`` receives the + + def list_plugin_distinfo(self): + """ return list of distinfo/plugin tuples for all setuptools registered + plugins. """ + return list(self._plugin_distinfo) + + def list_name_plugin(self): + """ return list of name/plugin pairs. """ + return list(self._name2plugin.items()) + + def get_hookcallers(self, plugin): + """ get all hook callers for the specified plugin. """ + return self._plugin2hookcallers.get(plugin) + + def add_hookcall_monitoring(self, before, after): + """ add before/after tracing functions for all hooks + and return an undo function which, when called, + will remove the added tracers. + + ``before(hook_name, hook_impls, kwargs)`` will be called ahead + of all hook calls and receive a hookcaller instance, a list + of HookImpl instances and the keyword arguments for the hook call. + + ``after(outcome, hook_name, hook_impls, kwargs)`` receives the same arguments as ``before`` but also a :py:class:`pluggy.callers._Result` object - which represents the result of the overall hook call. - """ + which represents the result of the overall hook call. + """ oldcall = self._inner_hookexec - + def traced_hookexec(hook, hook_impls, kwargs): before(hook.name, hook_impls, kwargs) outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs)) @@ -343,52 +343,52 @@ class PluginManager(object): return undo - def enable_tracing(self): - """ enable tracing of hook calls and return an undo function. """ + def enable_tracing(self): + """ enable tracing of hook calls and return an undo function. """ hooktrace = self.trace.root.get("hook") - - def before(hook_name, methods, kwargs): - hooktrace.root.indent += 1 - hooktrace(hook_name, kwargs) - - def after(outcome, hook_name, methods, kwargs): - if outcome.excinfo is None: - hooktrace("finish", hook_name, "-->", outcome.get_result()) - hooktrace.root.indent -= 1 - - return self.add_hookcall_monitoring(before, after) - - def subset_hook_caller(self, name, remove_plugins): + + def before(hook_name, methods, kwargs): + hooktrace.root.indent += 1 + hooktrace(hook_name, kwargs) + + def after(outcome, hook_name, methods, kwargs): + if outcome.excinfo is None: + hooktrace("finish", hook_name, "-->", outcome.get_result()) + hooktrace.root.indent -= 1 + + return self.add_hookcall_monitoring(before, after) + + def subset_hook_caller(self, name, remove_plugins): """ Return a new :py:class:`.hooks._HookCaller` instance for the named method - which manages calls to all registered plugins except the - ones from remove_plugins. """ - orig = getattr(self.hook, name) - plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)] - if plugins_to_remove: - hc = _HookCaller( - orig.name, orig._hookexec, orig.spec.namespace, orig.spec.opts - ) - for hookimpl in orig.get_hookimpls(): - plugin = hookimpl.plugin - if plugin not in plugins_to_remove: - hc._add_hookimpl(hookimpl) - # we also keep track of this hook caller so it - # gets properly removed on plugin unregistration - self._plugin2hookcallers.setdefault(plugin, []).append(hc) - return hc - return orig - - -if hasattr(inspect, "signature"): - - def _formatdef(func): - return "%s%s" % (func.__name__, str(inspect.signature(func))) - - -else: - - def _formatdef(func): - return "%s%s" % ( - func.__name__, - inspect.formatargspec(*inspect.getargspec(func)), - ) + which manages calls to all registered plugins except the + ones from remove_plugins. """ + orig = getattr(self.hook, name) + plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)] + if plugins_to_remove: + hc = _HookCaller( + orig.name, orig._hookexec, orig.spec.namespace, orig.spec.opts + ) + for hookimpl in orig.get_hookimpls(): + plugin = hookimpl.plugin + if plugin not in plugins_to_remove: + hc._add_hookimpl(hookimpl) + # we also keep track of this hook caller so it + # gets properly removed on plugin unregistration + self._plugin2hookcallers.setdefault(plugin, []).append(hc) + return hc + return orig + + +if hasattr(inspect, "signature"): + + def _formatdef(func): + return "%s%s" % (func.__name__, str(inspect.signature(func))) + + +else: + + def _formatdef(func): + return "%s%s" % ( + func.__name__, + inspect.formatargspec(*inspect.getargspec(func)), + ) diff --git a/contrib/python/pluggy/py2/ya.make b/contrib/python/pluggy/py2/ya.make index 28686a4db2..40ae96f0bf 100644 --- a/contrib/python/pluggy/py2/ya.make +++ b/contrib/python/pluggy/py2/ya.make @@ -1,34 +1,34 @@ PY2_LIBRARY() - + OWNER(g:python-contrib) - + VERSION(0.13.1) - + LICENSE(MIT) PEERDIR( contrib/python/importlib-metadata ) -NO_LINT() - -PY_SRCS( - TOP_LEVEL - pluggy/__init__.py - pluggy/_tracing.py - pluggy/_version.py - pluggy/callers.py - pluggy/hooks.py - pluggy/manager.py -) - +NO_LINT() + +PY_SRCS( + TOP_LEVEL + pluggy/__init__.py + pluggy/_tracing.py + pluggy/_version.py + pluggy/callers.py + pluggy/hooks.py + pluggy/manager.py +) + RESOURCE_FILES( PREFIX contrib/python/pluggy/py2/ .dist-info/METADATA .dist-info/top_level.txt ) -END() +END() RECURSE_FOR_TESTS( tests diff --git a/contrib/python/pluggy/ya.make b/contrib/python/pluggy/ya.make index 7e8cf49721..43e4c7b2ab 100644 --- a/contrib/python/pluggy/ya.make +++ b/contrib/python/pluggy/ya.make @@ -1,9 +1,9 @@ -PY23_LIBRARY() - +PY23_LIBRARY() + LICENSE(Service-Py23-Proxy) OWNER(g:python-contrib) - + IF (PYTHON2) PEERDIR(contrib/python/pluggy/py2) ELSE() @@ -11,8 +11,8 @@ ELSE() ENDIF() NO_LINT() - -END() + +END() RECURSE( py2 diff --git a/contrib/python/py/py/__init__.py b/contrib/python/py/py/__init__.py index e8d06b03a1..b892ce1a2a 100644 --- a/contrib/python/py/py/__init__.py +++ b/contrib/python/py/py/__init__.py @@ -1,5 +1,5 @@ """ -pylib: rapid testing and development utils +pylib: rapid testing and development utils this module uses apipkg.py for lazy-loading sub modules and classes. The initpkg-dictionary below specifies @@ -8,25 +8,25 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2014 """ -from py._error import error - -try: - from py._vendored_packages import apipkg - lib_not_mangled_by_packagers = True - vendor_prefix = '._vendored_packages.' -except ImportError: - import apipkg - lib_not_mangled_by_packagers = False - vendor_prefix = '' - -try: - from ._version import version as __version__ -except ImportError: - # broken installation, we don't even try - __version__ = "unknown" - - -apipkg.initpkg(__name__, attr={'_apipkg': apipkg, 'error': error}, exportdefs={ +from py._error import error + +try: + from py._vendored_packages import apipkg + lib_not_mangled_by_packagers = True + vendor_prefix = '._vendored_packages.' +except ImportError: + import apipkg + lib_not_mangled_by_packagers = False + vendor_prefix = '' + +try: + from ._version import version as __version__ +except ImportError: + # broken installation, we don't even try + __version__ = "unknown" + + +apipkg.initpkg(__name__, attr={'_apipkg': apipkg, 'error': error}, exportdefs={ # access to all standard lib modules 'std': '._std:std', @@ -46,13 +46,13 @@ apipkg.initpkg(__name__, attr={'_apipkg': apipkg, 'error': error}, exportdefs={ }, 'apipkg' : { - 'initpkg' : vendor_prefix + 'apipkg:initpkg', - 'ApiModule' : vendor_prefix + 'apipkg:ApiModule', + 'initpkg' : vendor_prefix + 'apipkg:initpkg', + 'ApiModule' : vendor_prefix + 'apipkg:ApiModule', }, 'iniconfig' : { - 'IniConfig' : vendor_prefix + 'iniconfig:IniConfig', - 'ParseError' : vendor_prefix + 'iniconfig:ParseError', + 'IniConfig' : vendor_prefix + 'iniconfig:IniConfig', + 'ParseError' : vendor_prefix + 'iniconfig:ParseError', }, 'path' : { diff --git a/contrib/python/py/py/_code/_assertionnew.py b/contrib/python/py/py/_code/_assertionnew.py index bc7d1292cc..d03f29d870 100644 --- a/contrib/python/py/py/_code/_assertionnew.py +++ b/contrib/python/py/py/_code/_assertionnew.py @@ -10,10 +10,10 @@ import py from py._code.assertion import _format_explanation, BuiltinAssertionError -def _is_ast_expr(node): - return isinstance(node, ast.expr) -def _is_ast_stmt(node): - return isinstance(node, ast.stmt) +def _is_ast_expr(node): + return isinstance(node, ast.expr) +def _is_ast_stmt(node): + return isinstance(node, ast.stmt) class Failure(Exception): diff --git a/contrib/python/py/py/_code/_assertionold.py b/contrib/python/py/py/_code/_assertionold.py index 041616bacf..1bb70a875d 100644 --- a/contrib/python/py/py/_code/_assertionold.py +++ b/contrib/python/py/py/_code/_assertionold.py @@ -2,7 +2,7 @@ import py import sys, inspect from compiler import parse, ast, pycodegen from py._code.assertion import BuiltinAssertionError, _format_explanation -import types +import types passthroughex = py.builtin._sysex @@ -471,7 +471,7 @@ def check(s, frame=None): def interpret(source, frame, should_fail=False): module = Interpretable(parse(source, 'exec').node) #print "got module", module - if isinstance(frame, types.FrameType): + if isinstance(frame, types.FrameType): frame = py.code.Frame(frame) try: module.run(frame) diff --git a/contrib/python/py/py/_code/assertion.py b/contrib/python/py/py/_code/assertion.py index a72b8a2eaf..ff1643799c 100644 --- a/contrib/python/py/py/_code/assertion.py +++ b/contrib/python/py/py/_code/assertion.py @@ -87,4 +87,4 @@ if sys.version_info > (3, 0): reinterpret_old = "old reinterpretation not available for py3" else: from py._code._assertionold import interpret as reinterpret_old -from py._code._assertionnew import interpret as reinterpret +from py._code._assertionnew import interpret as reinterpret diff --git a/contrib/python/py/py/_code/code.py b/contrib/python/py/py/_code/code.py index 6478dbad88..dad796283f 100644 --- a/contrib/python/py/py/_code/code.py +++ b/contrib/python/py/py/_code/code.py @@ -1,6 +1,6 @@ import py import sys -from inspect import CO_VARARGS, CO_VARKEYWORDS, isclass +from inspect import CO_VARARGS, CO_VARKEYWORDS, isclass builtin_repr = repr @@ -11,9 +11,9 @@ if sys.version_info[0] >= 3: else: from py._code._py2traceback import format_exception_only -import traceback - - +import traceback + + class Code(object): """ wrapper around Python code objects """ def __init__(self, rawcode): @@ -24,7 +24,7 @@ class Code(object): self.firstlineno = rawcode.co_firstlineno - 1 self.name = rawcode.co_name except AttributeError: - raise TypeError("not a code object: %r" % (rawcode,)) + raise TypeError("not a code object: %r" % (rawcode,)) self.raw = rawcode def __eq__(self, other): @@ -109,7 +109,7 @@ class Frame(object): """ f_locals = self.f_locals.copy() f_locals.update(vars) - py.builtin.exec_(code, self.f_globals, f_locals) + py.builtin.exec_(code, self.f_globals, f_locals) def repr(self, object): """ return a 'safe' (non-recursive, one-line) string repr for 'object' @@ -133,7 +133,7 @@ class Frame(object): pass # this can occur when using Psyco return retval - + class TracebackEntry(object): """ a single entry in a traceback """ @@ -157,7 +157,7 @@ class TracebackEntry(object): return self.lineno - self.frame.code.firstlineno def __repr__(self): - return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno+1) + return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno+1) @property def statement(self): @@ -241,19 +241,19 @@ class TracebackEntry(object): raise except: line = "???" - return " File %r:%d in %s\n %s\n" % (fn, self.lineno+1, name, line) + return " File %r:%d in %s\n %s\n" % (fn, self.lineno+1, name, line) def name(self): return self.frame.code.raw.co_name name = property(name, None, None, "co_name of underlaying code") - + class Traceback(list): """ Traceback objects encapsulate and offer higher level access to Traceback entries. """ Entry = TracebackEntry - + def __init__(self, tb): """ initialize from given python traceback object. """ if hasattr(tb, 'tb_next'): @@ -368,8 +368,8 @@ class ExceptionInfo(object): self.traceback = py.code.Traceback(self.tb) def __repr__(self): - return "<ExceptionInfo %s tblen=%d>" % ( - self.typename, len(self.traceback)) + return "<ExceptionInfo %s tblen=%d>" % ( + self.typename, len(self.traceback)) def exconly(self, tryshort=False): """ return the exception as a string @@ -398,7 +398,7 @@ class ExceptionInfo(object): return ReprFileLocation(path, lineno+1, exconly) def getrepr(self, showlocals=False, style="long", - abspath=False, tbfilter=True, funcargs=False): + abspath=False, tbfilter=True, funcargs=False): """ return str()able representation of this exception info. showlocals: show locals per traceback entry style: long|short|no|native traceback style @@ -408,14 +408,14 @@ class ExceptionInfo(object): """ if style == 'native': return ReprExceptionInfo(ReprTracebackNative( - traceback.format_exception( + traceback.format_exception( self.type, self.value, self.traceback[0]._rawentry, )), self._getreprcrash()) - fmt = FormattedExcinfo( - showlocals=showlocals, style=style, + fmt = FormattedExcinfo( + showlocals=showlocals, style=style, abspath=abspath, tbfilter=tbfilter, funcargs=funcargs) return fmt.repr_excinfo(self) @@ -427,7 +427,7 @@ class ExceptionInfo(object): def __unicode__(self): entry = self.traceback[-1] loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return loc.__unicode__() + return loc.__unicode__() class FormattedExcinfo(object): @@ -436,8 +436,8 @@ class FormattedExcinfo(object): flow_marker = ">" fail_marker = "E" - def __init__(self, showlocals=False, style="long", - abspath=True, tbfilter=True, funcargs=False): + def __init__(self, showlocals=False, style="long", + abspath=True, tbfilter=True, funcargs=False): self.showlocals = showlocals self.style = style self.tbfilter = tbfilter @@ -530,7 +530,7 @@ class FormattedExcinfo(object): #else: # self._line("%-10s =\\" % (name,)) # # XXX - # pprint.pprint(value, stream=self.excinfowriter) + # pprint.pprint(value, stream=self.excinfowriter) return ReprLocals(lines) def repr_traceback_entry(self, entry, excinfo=None): @@ -788,7 +788,7 @@ def getrawcode(obj, trycall=True): obj = getattr(obj, 'f_code', obj) obj = getattr(obj, '__code__', obj) if trycall and not hasattr(obj, 'co_firstlineno'): - if hasattr(obj, '__call__') and not isclass(obj): + if hasattr(obj, '__call__') and not isclass(obj): x = getrawcode(obj.__call__, trycall=False) if hasattr(x, 'co_firstlineno'): return x diff --git a/contrib/python/py/py/_code/source.py b/contrib/python/py/py/_code/source.py index 419c6d9a1d..7fc7b23a96 100644 --- a/contrib/python/py/py/_code/source.py +++ b/contrib/python/py/py/_code/source.py @@ -193,8 +193,8 @@ class Source(object): if flag & _AST_FLAG: return co lines = [(x + "\n") for x in self.lines] - import linecache - linecache.cache[filename] = (1, None, lines, filename) + import linecache + linecache.cache[filename] = (1, None, lines, filename) return co # @@ -225,8 +225,8 @@ def getfslineno(obj): code = py.code.Code(obj) except TypeError: try: - fn = (inspect.getsourcefile(obj) or - inspect.getfile(obj)) + fn = (inspect.getsourcefile(obj) or + inspect.getfile(obj)) except TypeError: return "", -1 @@ -249,7 +249,7 @@ def getfslineno(obj): def findsource(obj): try: - sourcelines, lineno = inspect.findsource(obj) + sourcelines, lineno = inspect.findsource(obj) except py.builtin._sysex: raise except: diff --git a/contrib/python/py/py/_error.py b/contrib/python/py/py/_error.py index f9f4d4c4d1..a6375de9fa 100644 --- a/contrib/python/py/py/_error.py +++ b/contrib/python/py/py/_error.py @@ -2,7 +2,7 @@ create errno-specific classes for IO or os calls. """ -from types import ModuleType +from types import ModuleType import sys, os, errno class Error(EnvironmentError): @@ -24,7 +24,7 @@ _winerrnomap = { 2: errno.ENOENT, 3: errno.ENOENT, 17: errno.EEXIST, - 18: errno.EXDEV, + 18: errno.EXDEV, 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable 22: errno.ENOTDIR, 20: errno.ENOTDIR, @@ -32,7 +32,7 @@ _winerrnomap = { 5: errno.EACCES, # anything better? } -class ErrorMaker(ModuleType): +class ErrorMaker(ModuleType): """ lazily provides Exception classes for each possible POSIX errno (as defined per the 'errno' module). All such instances subclass EnvironmentError. @@ -87,5 +87,5 @@ class ErrorMaker(ModuleType): __tracebackhide__ = True -error = ErrorMaker('py.error') -sys.modules[error.__name__] = error
\ No newline at end of file +error = ErrorMaker('py.error') +sys.modules[error.__name__] = error
\ No newline at end of file diff --git a/contrib/python/py/py/_io/terminalwriter.py b/contrib/python/py/py/_io/terminalwriter.py index cffef98006..442ca2395e 100644 --- a/contrib/python/py/py/_io/terminalwriter.py +++ b/contrib/python/py/py/_io/terminalwriter.py @@ -5,10 +5,10 @@ Helper functions for writing to terminals and files. """ -import sys, os, unicodedata +import sys, os, unicodedata import py py3k = sys.version_info[0] >= 3 -py33 = sys.version_info >= (3, 3) +py33 = sys.version_info >= (3, 3) from py.builtin import text, bytes win32_and_ctypes = False @@ -25,21 +25,21 @@ if sys.platform == "win32": def _getdimensions(): - if py33: - import shutil - size = shutil.get_terminal_size() - return size.lines, size.columns - else: - import termios, fcntl, struct - call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) - height, width = struct.unpack("hhhh", call)[:2] - return height, width + if py33: + import shutil + size = shutil.get_terminal_size() + return size.lines, size.columns + else: + import termios, fcntl, struct + call = fcntl.ioctl(1, termios.TIOCGWINSZ, "\000" * 8) + height, width = struct.unpack("hhhh", call)[:2] + return height, width def get_terminal_width(): - width = 0 + width = 0 try: - _, width = _getdimensions() + _, width = _getdimensions() except py.builtin._sysex: raise except: @@ -59,21 +59,21 @@ def get_terminal_width(): terminal_width = get_terminal_width() -char_width = { - 'A': 1, # "Ambiguous" - 'F': 2, # Fullwidth - 'H': 1, # Halfwidth - 'N': 1, # Neutral - 'Na': 1, # Narrow - 'W': 2, # Wide -} - - -def get_line_width(text): - text = unicodedata.normalize('NFC', text) - return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) - - +char_width = { + 'A': 1, # "Ambiguous" + 'F': 2, # Fullwidth + 'H': 1, # Halfwidth + 'N': 1, # Neutral + 'Na': 1, # Narrow + 'W': 2, # Wide +} + + +def get_line_width(text): + text = unicodedata.normalize('NFC', text) + return sum(char_width.get(unicodedata.east_asian_width(c), 1) for c in text) + + # XXX unify with _escaped func below def ansi_print(text, esc, file=None, newline=True, flush=False): if file is None: @@ -152,7 +152,7 @@ class TerminalWriter(object): if stringio: self.stringio = file = py.io.TextIO() else: - from sys import stdout as file + from sys import stdout as file elif py.builtin.callable(file) and not ( hasattr(file, "write") and hasattr(file, "flush")): file = WriteFile(file, encoding=encoding) @@ -162,42 +162,42 @@ class TerminalWriter(object): self._file = file self.hasmarkup = should_do_markup(file) self._lastlen = 0 - self._chars_on_current_line = 0 - self._width_of_current_line = 0 - - @property - def fullwidth(self): - if hasattr(self, '_terminal_width'): - return self._terminal_width - return get_terminal_width() - - @fullwidth.setter - def fullwidth(self, value): - self._terminal_width = value - - @property - def chars_on_current_line(self): - """Return the number of characters written so far in the current line. - - Please note that this count does not produce correct results after a reline() call, - see #164. - - .. versionadded:: 1.5.0 - - :rtype: int - """ - return self._chars_on_current_line - - @property - def width_of_current_line(self): - """Return an estimate of the width so far in the current line. - - .. versionadded:: 1.6.0 - - :rtype: int - """ - return self._width_of_current_line - + self._chars_on_current_line = 0 + self._width_of_current_line = 0 + + @property + def fullwidth(self): + if hasattr(self, '_terminal_width'): + return self._terminal_width + return get_terminal_width() + + @fullwidth.setter + def fullwidth(self, value): + self._terminal_width = value + + @property + def chars_on_current_line(self): + """Return the number of characters written so far in the current line. + + Please note that this count does not produce correct results after a reline() call, + see #164. + + .. versionadded:: 1.5.0 + + :rtype: int + """ + return self._chars_on_current_line + + @property + def width_of_current_line(self): + """Return an estimate of the width so far in the current line. + + .. versionadded:: 1.6.0 + + :rtype: int + """ + return self._width_of_current_line + def _escaped(self, text, esc): if esc and self.hasmarkup: text = (''.join(['\x1b[%sm' % cod for cod in esc]) + @@ -248,27 +248,27 @@ class TerminalWriter(object): if msg: if not isinstance(msg, (bytes, text)): msg = text(msg) - - self._update_chars_on_current_line(msg) - + + self._update_chars_on_current_line(msg) + if self.hasmarkup and kw: markupmsg = self.markup(msg, **kw) else: markupmsg = msg write_out(self._file, markupmsg) - def _update_chars_on_current_line(self, text_or_bytes): - newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' - current_line = text_or_bytes.rsplit(newline, 1)[-1] - if isinstance(current_line, bytes): - current_line = current_line.decode('utf-8', errors='replace') - if newline in text_or_bytes: - self._chars_on_current_line = len(current_line) - self._width_of_current_line = get_line_width(current_line) - else: - self._chars_on_current_line += len(current_line) - self._width_of_current_line += get_line_width(current_line) - + def _update_chars_on_current_line(self, text_or_bytes): + newline = b'\n' if isinstance(text_or_bytes, bytes) else '\n' + current_line = text_or_bytes.rsplit(newline, 1)[-1] + if isinstance(current_line, bytes): + current_line = current_line.decode('utf-8', errors='replace') + if newline in text_or_bytes: + self._chars_on_current_line = len(current_line) + self._width_of_current_line = get_line_width(current_line) + else: + self._chars_on_current_line += len(current_line) + self._width_of_current_line += get_line_width(current_line) + def line(self, s='', **kw): self.write(s, **kw) self._checkfill(s) @@ -292,9 +292,9 @@ class Win32ConsoleWriter(TerminalWriter): if msg: if not isinstance(msg, (bytes, text)): msg = text(msg) - - self._update_chars_on_current_line(msg) - + + self._update_chars_on_current_line(msg) + oldcolors = None if self.hasmarkup and kw: handle = GetStdHandle(STD_OUTPUT_HANDLE) diff --git a/contrib/python/py/py/_log/log.py b/contrib/python/py/py/_log/log.py index 873fd0a6da..56969bcb58 100644 --- a/contrib/python/py/py/_log/log.py +++ b/contrib/python/py/py/_log/log.py @@ -14,10 +14,10 @@ XXX implement this API: (maybe put it into slogger.py?) debug=py.log.STDOUT, command=None) """ -import py -import sys +import py +import sys + - class Message(object): def __init__(self, keywords, args): self.keywords = keywords @@ -72,7 +72,7 @@ class KeywordMapper: def getstate(self): return self.keywords2consumer.copy() - + def setstate(self, state): self.keywords2consumer.clear() self.keywords2consumer.update(state) @@ -107,22 +107,22 @@ class KeywordMapper: consumer = File(consumer) self.keywords2consumer[keywords] = consumer - + def default_consumer(msg): """ the default consumer, prints the message to stdout (using 'print') """ sys.stderr.write(str(msg)+"\n") default_keywordmapper = KeywordMapper() - + def setconsumer(keywords, consumer): default_keywordmapper.setconsumer(keywords, consumer) - + def setstate(state): default_keywordmapper.setstate(state) - - + + def getstate(): return default_keywordmapper.getstate() @@ -130,12 +130,12 @@ def getstate(): # Consumers # - + class File(object): """ log consumer wrapping a file(-like) object """ def __init__(self, f): assert hasattr(f, 'write') - # assert isinstance(f, file) or not hasattr(f, 'open') + # assert isinstance(f, file) or not hasattr(f, 'open') self._file = f def __call__(self, msg): @@ -144,7 +144,7 @@ class File(object): if hasattr(self._file, 'flush'): self._file.flush() - + class Path(object): """ log consumer that opens and writes to a Path """ def __init__(self, filename, append=False, @@ -168,39 +168,39 @@ class Path(object): if not self._buffering: self._file.flush() - + def STDOUT(msg): """ consumer that writes to sys.stdout """ sys.stdout.write(str(msg)+"\n") - + def STDERR(msg): """ consumer that writes to sys.stderr """ sys.stderr.write(str(msg)+"\n") - + class Syslog: """ consumer that writes to the syslog daemon """ - def __init__(self, priority=None): + def __init__(self, priority=None): if priority is None: priority = self.LOG_INFO self.priority = priority def __call__(self, msg): """ write a message to the log """ - import syslog - syslog.syslog(self.priority, str(msg)) - - -try: - import syslog -except ImportError: - pass -else: - for _prio in "EMERG ALERT CRIT ERR WARNING NOTICE INFO DEBUG".split(): - _prio = "LOG_" + _prio - try: - setattr(Syslog, _prio, getattr(syslog, _prio)) - except AttributeError: - pass + import syslog + syslog.syslog(self.priority, str(msg)) + + +try: + import syslog +except ImportError: + pass +else: + for _prio in "EMERG ALERT CRIT ERR WARNING NOTICE INFO DEBUG".split(): + _prio = "LOG_" + _prio + try: + setattr(Syslog, _prio, getattr(syslog, _prio)) + except AttributeError: + pass diff --git a/contrib/python/py/py/_log/warning.py b/contrib/python/py/py/_log/warning.py index c5ee3ef597..6ef20d98a2 100644 --- a/contrib/python/py/py/_log/warning.py +++ b/contrib/python/py/py/_log/warning.py @@ -32,11 +32,11 @@ def _apiwarn(startversion, msg, stacklevel=2, function=None): msg = "%s (since version %s)" %(msg, startversion) warn(msg, stacklevel=stacklevel+1, function=function) - + def warn(msg, stacklevel=1, function=None): if function is not None: - import inspect - filename = inspect.getfile(function) + import inspect + filename = inspect.getfile(function) lineno = py.code.getrawcode(function).co_firstlineno else: try: @@ -69,11 +69,11 @@ def warn(msg, stacklevel=1, function=None): filename = module path = py.path.local(filename) warning = DeprecationWarning(msg, path, lineno) - import warnings - warnings.warn_explicit(warning, category=Warning, + import warnings + warnings.warn_explicit(warning, category=Warning, filename=str(warning.path), lineno=warning.lineno, - registry=warnings.__dict__.setdefault( + registry=warnings.__dict__.setdefault( "__warningsregistry__", {}) ) diff --git a/contrib/python/py/py/_path/common.py b/contrib/python/py/py/_path/common.py index a7408dbfe6..2364e5fef5 100644 --- a/contrib/python/py/py/_path/common.py +++ b/contrib/python/py/py/_path/common.py @@ -1,55 +1,55 @@ """ """ -import warnings -import os -import sys -import posixpath -import fnmatch +import warnings +import os +import sys +import posixpath +import fnmatch import py # Moved from local.py. iswin32 = sys.platform == "win32" or (getattr(os, '_name', False) == 'nt') -try: +try: # FileNotFoundError might happen in py34, and is not available with py27. import_errors = (ImportError, FileNotFoundError) except NameError: import_errors = (ImportError,) try: - from os import fspath -except ImportError: - def fspath(path): - """ - Return the string representation of the path. - If str or bytes is passed in, it is returned unchanged. - This code comes from PEP 519, modified to support earlier versions of - python. - - This is required for python < 3.6. - """ - if isinstance(path, (py.builtin.text, py.builtin.bytes)): - return path - - # Work from the object's type to match method resolution of other magic - # methods. - path_type = type(path) - try: - return path_type.__fspath__(path) - except AttributeError: - if hasattr(path_type, '__fspath__'): - raise - try: - import pathlib + from os import fspath +except ImportError: + def fspath(path): + """ + Return the string representation of the path. + If str or bytes is passed in, it is returned unchanged. + This code comes from PEP 519, modified to support earlier versions of + python. + + This is required for python < 3.6. + """ + if isinstance(path, (py.builtin.text, py.builtin.bytes)): + return path + + # Work from the object's type to match method resolution of other magic + # methods. + path_type = type(path) + try: + return path_type.__fspath__(path) + except AttributeError: + if hasattr(path_type, '__fspath__'): + raise + try: + import pathlib except import_errors: - pass - else: - if isinstance(path, pathlib.PurePath): - return py.builtin.text(path) - - raise TypeError("expected str, bytes or os.PathLike object, not " - + path_type.__name__) - + pass + else: + if isinstance(path, pathlib.PurePath): + return py.builtin.text(path) + + raise TypeError("expected str, bytes or os.PathLike object, not " + + path_type.__name__) + class Checkers: _depend_on_existence = 'exists', 'link', 'dir', 'file' @@ -133,7 +133,7 @@ class PathBase(object): Checkers = Checkers def __div__(self, other): - return self.join(fspath(other)) + return self.join(fspath(other)) __truediv__ = __div__ # py3k def basename(self): @@ -179,16 +179,16 @@ class PathBase(object): def readlines(self, cr=1): """ read and return a list of lines from the path. if cr is False, the newline will be removed from the end of each line. """ - if sys.version_info < (3, ): - mode = 'rU' - else: # python 3 deprecates mode "U" in favor of "newline" option - mode = 'r' - + if sys.version_info < (3, ): + mode = 'rU' + else: # python 3 deprecates mode "U" in favor of "newline" option + mode = 'r' + if not cr: - content = self.read(mode) + content = self.read(mode) return content.split('\n') else: - f = self.open(mode) + f = self.open(mode) try: return f.readlines() finally: @@ -198,16 +198,16 @@ newline will be removed from the end of each line. """ """ (deprecated) return object unpickled from self.read() """ f = self.open('rb') try: - import pickle - return py.error.checked_call(pickle.load, f) + import pickle + return py.error.checked_call(pickle.load, f) finally: f.close() def move(self, target): """ move this path to target. """ if target.relto(self): - raise py.error.EINVAL( - target, + raise py.error.EINVAL( + target, "cannot move path into a subdirectory of itself") try: self.rename(target) @@ -237,7 +237,7 @@ newline will be removed from the end of each line. """ path.check(file=1, link=1) # a link pointing to a file """ if not kw: - kw = {'exists': 1} + kw = {'exists': 1} return self.Checkers(self)._evaluate(kw) def fnmatch(self, pattern): @@ -270,7 +270,7 @@ newline will be removed from the end of each line. """ strrelpath += self.sep #assert strrelpath[-1] == self.sep #assert strrelpath[-2] != self.sep - strself = self.strpath + strself = self.strpath if sys.platform == "win32" or getattr(os, '_name', None) == 'nt': if os.path.normcase(strself).startswith( os.path.normcase(strrelpath)): @@ -386,9 +386,9 @@ newline will be removed from the end of each line. """ def _sortlist(self, res, sort): if sort: if hasattr(sort, '__call__'): - warnings.warn(DeprecationWarning( - "listdir(sort=callable) is deprecated and breaks on python3" - ), stacklevel=3) + warnings.warn(DeprecationWarning( + "listdir(sort=callable) is deprecated and breaks on python3" + ), stacklevel=3) res.sort(sort) else: res.sort() @@ -397,14 +397,14 @@ newline will be removed from the end of each line. """ """ return True if other refers to the same stat object as self. """ return self.strpath == str(other) - def __fspath__(self): - return self.strpath - + def __fspath__(self): + return self.strpath + class Visitor: def __init__(self, fil, rec, ignore, bf, sort): - if isinstance(fil, py.builtin._basestring): + if isinstance(fil, py.builtin._basestring): fil = FNMatcher(fil) - if isinstance(rec, py.builtin._basestring): + if isinstance(rec, py.builtin._basestring): self.rec = FNMatcher(rec) elif not hasattr(rec, '__call__') and rec: self.rec = lambda path: True @@ -456,4 +456,4 @@ class FNMatcher: name = str(path) # path.strpath # XXX svn? if not os.path.isabs(pattern): pattern = '*' + path.sep + pattern - return fnmatch.fnmatch(name, pattern) + return fnmatch.fnmatch(name, pattern) diff --git a/contrib/python/py/py/_path/local.py b/contrib/python/py/py/_path/local.py index 22542bd5ff..1385a03987 100644 --- a/contrib/python/py/py/_path/local.py +++ b/contrib/python/py/py/_path/local.py @@ -4,10 +4,10 @@ local path implementation. from __future__ import with_statement from contextlib import contextmanager -import sys, os, atexit, io, uuid +import sys, os, atexit, io, uuid import py from py._path import common -from py._path.common import iswin32, fspath +from py._path.common import iswin32, fspath from stat import S_ISLNK, S_ISDIR, S_ISREG from os.path import abspath, normpath, isabs, exists, isdir, isfile, islink, dirname @@ -152,12 +152,12 @@ class LocalPath(FSBase): """ if path is None: self.strpath = py.error.checked_call(os.getcwd) - else: - try: - path = fspath(path) - except TypeError: - raise ValueError("can only pass None, Path instances " - "or non-empty strings to LocalPath") + else: + try: + path = fspath(path) + except TypeError: + raise ValueError("can only pass None, Path instances " + "or non-empty strings to LocalPath") if expanduser: path = os.path.expanduser(path) self.strpath = abspath(path) @@ -169,11 +169,11 @@ class LocalPath(FSBase): return hash(s) def __eq__(self, other): - s1 = fspath(self) - try: - s2 = fspath(other) - except TypeError: - return False + s1 = fspath(self) + try: + s2 = fspath(other) + except TypeError: + return False if iswin32: s1 = s1.lower() try: @@ -186,15 +186,15 @@ class LocalPath(FSBase): return not (self == other) def __lt__(self, other): - return fspath(self) < fspath(other) + return fspath(self) < fspath(other) def __gt__(self, other): - return fspath(self) > fspath(other) + return fspath(self) > fspath(other) def samefile(self, other): """ return True if 'other' references the same file as 'self'. """ - other = fspath(other) + other = fspath(other) if not isabs(other): other = abspath(other) if self == other: @@ -213,16 +213,16 @@ class LocalPath(FSBase): if rec: # force remove of readonly files on windows if iswin32: - self.chmod(0o700, rec=1) - import shutil - py.error.checked_call( - shutil.rmtree, self.strpath, + self.chmod(0o700, rec=1) + import shutil + py.error.checked_call( + shutil.rmtree, self.strpath, ignore_errors=ignore_errors) else: py.error.checked_call(os.rmdir, self.strpath) else: if iswin32: - self.chmod(0o700) + self.chmod(0o700) py.error.checked_call(os.remove, self.strpath) def computehash(self, hashtype="md5", chunksize=524288): @@ -333,7 +333,7 @@ class LocalPath(FSBase): of the args is an absolute path. """ sep = self.sep - strargs = [fspath(arg) for arg in args] + strargs = [fspath(arg) for arg in args] strpath = self.strpath if kwargs.get('abs'): newargs = [] @@ -343,16 +343,16 @@ class LocalPath(FSBase): strargs = newargs break newargs.insert(0, arg) - # special case for when we have e.g. strpath == "/" - actual_sep = "" if strpath.endswith(sep) else sep + # special case for when we have e.g. strpath == "/" + actual_sep = "" if strpath.endswith(sep) else sep for arg in strargs: arg = arg.strip(sep) if iswin32: # allow unix style paths even on windows. arg = arg.strip('/') arg = arg.replace('/', sep) - strpath = strpath + actual_sep + arg - actual_sep = sep + strpath = strpath + actual_sep + arg + actual_sep = sep obj = object.__new__(self.__class__) obj.strpath = normpath(strpath) return obj @@ -418,22 +418,22 @@ class LocalPath(FSBase): """ return last modification time of the path. """ return self.stat().mtime - def copy(self, target, mode=False, stat=False): - """ copy path to target. - - If mode is True, will copy copy permission from path to target. - If stat is True, copy permission, last modification - time, last access time, and flags from path to target. - """ + def copy(self, target, mode=False, stat=False): + """ copy path to target. + + If mode is True, will copy copy permission from path to target. + If stat is True, copy permission, last modification + time, last access time, and flags from path to target. + """ if self.check(file=1): if target.check(dir=1): target = target.join(self.basename) assert self!=target copychunked(self, target) if mode: - copymode(self.strpath, target.strpath) - if stat: - copystat(self, target) + copymode(self.strpath, target.strpath) + if stat: + copystat(self, target) else: def rec(p): return p.check(link=0) @@ -449,28 +449,28 @@ class LocalPath(FSBase): elif x.check(dir=1): newx.ensure(dir=1) if mode: - copymode(x.strpath, newx.strpath) - if stat: - copystat(x, newx) + copymode(x.strpath, newx.strpath) + if stat: + copystat(x, newx) def rename(self, target): """ rename this path to target. """ - target = fspath(target) + target = fspath(target) return py.error.checked_call(os.rename, self.strpath, target) def dump(self, obj, bin=1): """ pickle object into path location""" f = self.open('wb') - import pickle + import pickle try: - py.error.checked_call(pickle.dump, obj, f, bin) + py.error.checked_call(pickle.dump, obj, f, bin) finally: f.close() def mkdir(self, *args): """ create & return the directory joined with args. """ p = self.join(*args) - py.error.checked_call(os.mkdir, fspath(p)) + py.error.checked_call(os.mkdir, fspath(p)) return p def write_binary(self, data, ensure=False): @@ -619,7 +619,7 @@ class LocalPath(FSBase): if rec: for x in self.visit(rec=rec): py.error.checked_call(os.chmod, str(x), mode) - py.error.checked_call(os.chmod, self.strpath, mode) + py.error.checked_call(os.chmod, self.strpath, mode) def pypkgpath(self): """ return the Python package path by looking for the last @@ -705,7 +705,7 @@ class LocalPath(FSBase): mod = sys.modules[modname] if self.basename == "__init__.py": return mod # we don't check anything as we might - # be in a namespace package ... too icky to check + # be in a namespace package ... too icky to check modfile = mod.__file__ if modfile[-4:] in ('.pyc', '.pyo'): modfile = modfile[:-1] @@ -719,17 +719,17 @@ class LocalPath(FSBase): except py.error.ENOENT: issame = False if not issame: - ignore = os.getenv('PY_IGNORE_IMPORTMISMATCH') - if ignore != '1': - raise self.ImportMismatchError(modname, modfile, self) + ignore = os.getenv('PY_IGNORE_IMPORTMISMATCH') + if ignore != '1': + raise self.ImportMismatchError(modname, modfile, self) return mod else: try: return sys.modules[modname] except KeyError: # we have a custom modname, do a pseudo-import - import types - mod = types.ModuleType(modname) + import types + mod = types.ModuleType(modname) mod.__file__ = str(self) sys.modules[modname] = mod try: @@ -774,7 +774,7 @@ class LocalPath(FSBase): else: if paths is None: if iswin32: - paths = os.environ['Path'].split(';') + paths = os.environ['Path'].split(';') if '' not in paths and '.' not in paths: paths.append('.') try: @@ -782,10 +782,10 @@ class LocalPath(FSBase): except KeyError: pass else: - paths = [path.replace('%SystemRoot%', systemroot) + paths = [path.replace('%SystemRoot%', systemroot) for path in paths] else: - paths = os.environ['PATH'].split(':') + paths = os.environ['PATH'].split(':') tryadd = [] if iswin32: tryadd += os.environ['PATHEXT'].split(os.pathsep) @@ -816,18 +816,18 @@ class LocalPath(FSBase): return cls(x) _gethomedir = classmethod(_gethomedir) - # """ - # special class constructors for local filesystem paths - # """ - @classmethod + # """ + # special class constructors for local filesystem paths + # """ + @classmethod def get_temproot(cls): """ return the system's temporary directory (where tempfiles are usually created in) """ - import tempfile - return py.path.local(tempfile.gettempdir()) + import tempfile + return py.path.local(tempfile.gettempdir()) - @classmethod + @classmethod def mkdtemp(cls, rootdir=None): """ return a Path object pointing to a fresh new temporary directory (which we created ourself). @@ -838,41 +838,41 @@ class LocalPath(FSBase): return cls(py.error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) def make_numbered_dir(cls, prefix='session-', rootdir=None, keep=3, - lock_timeout=172800): # two days + lock_timeout=172800): # two days """ return unique directory with a number greater than the current maximum one. The number is assumed to start directly after prefix. if keep is true directories with a number less than (maxnum-keep) - will be removed. If .lock files are used (lock_timeout non-zero), - algorithm is multi-process safe. + will be removed. If .lock files are used (lock_timeout non-zero), + algorithm is multi-process safe. """ if rootdir is None: rootdir = cls.get_temproot() - nprefix = prefix.lower() + nprefix = prefix.lower() def parse_num(path): """ parse the number out of a path (if it matches the prefix) """ - nbasename = path.basename.lower() - if nbasename.startswith(nprefix): + nbasename = path.basename.lower() + if nbasename.startswith(nprefix): try: - return int(nbasename[len(nprefix):]) + return int(nbasename[len(nprefix):]) except ValueError: pass - def create_lockfile(path): - """ exclusively create lockfile. Throws when failed """ + def create_lockfile(path): + """ exclusively create lockfile. Throws when failed """ mypid = os.getpid() - lockfile = path.join('.lock') + lockfile = path.join('.lock') if hasattr(lockfile, 'mksymlinkto'): lockfile.mksymlinkto(str(mypid)) else: - fd = py.error.checked_call(os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) - with os.fdopen(fd, 'w') as f: - f.write(str(mypid)) - return lockfile - - def atexit_remove_lockfile(lockfile): - """ ensure lockfile is removed at process exit """ - mypid = os.getpid() + fd = py.error.checked_call(os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) + with os.fdopen(fd, 'w') as f: + f.write(str(mypid)) + return lockfile + + def atexit_remove_lockfile(lockfile): + """ ensure lockfile is removed at process exit """ + mypid = os.getpid() def try_remove_lockfile(): # in a fork() situation, only the last process should # remove the .lock, otherwise the other processes run the @@ -887,83 +887,83 @@ class LocalPath(FSBase): pass atexit.register(try_remove_lockfile) - # compute the maximum number currently in use with the prefix - lastmax = None - while True: - maxnum = -1 - for path in rootdir.listdir(): - num = parse_num(path) - if num is not None: - maxnum = max(maxnum, num) - - # make the new directory - try: - udir = rootdir.mkdir(prefix + str(maxnum+1)) - if lock_timeout: - lockfile = create_lockfile(udir) - atexit_remove_lockfile(lockfile) - except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): - # race condition (1): another thread/process created the dir - # in the meantime - try again - # race condition (2): another thread/process spuriously acquired - # lock treating empty directory as candidate - # for removal - try again - # race condition (3): another thread/process tried to create the lock at - # the same time (happened in Python 3.3 on Windows) - # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa - if lastmax == maxnum: - raise - lastmax = maxnum - continue - break - - def get_mtime(path): - """ read file modification time """ - try: - return path.lstat().mtime - except py.error.Error: - pass - - garbage_prefix = prefix + 'garbage-' - - def is_garbage(path): - """ check if path denotes directory scheduled for removal """ - bn = path.basename - return bn.startswith(garbage_prefix) - + # compute the maximum number currently in use with the prefix + lastmax = None + while True: + maxnum = -1 + for path in rootdir.listdir(): + num = parse_num(path) + if num is not None: + maxnum = max(maxnum, num) + + # make the new directory + try: + udir = rootdir.mkdir(prefix + str(maxnum+1)) + if lock_timeout: + lockfile = create_lockfile(udir) + atexit_remove_lockfile(lockfile) + except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): + # race condition (1): another thread/process created the dir + # in the meantime - try again + # race condition (2): another thread/process spuriously acquired + # lock treating empty directory as candidate + # for removal - try again + # race condition (3): another thread/process tried to create the lock at + # the same time (happened in Python 3.3 on Windows) + # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa + if lastmax == maxnum: + raise + lastmax = maxnum + continue + break + + def get_mtime(path): + """ read file modification time """ + try: + return path.lstat().mtime + except py.error.Error: + pass + + garbage_prefix = prefix + 'garbage-' + + def is_garbage(path): + """ check if path denotes directory scheduled for removal """ + bn = path.basename + return bn.startswith(garbage_prefix) + # prune old directories - udir_time = get_mtime(udir) - if keep and udir_time: + udir_time = get_mtime(udir) + if keep and udir_time: for path in rootdir.listdir(): num = parse_num(path) if num is not None and num <= (maxnum - keep): try: - # try acquiring lock to remove directory as exclusive user - if lock_timeout: - create_lockfile(path) - except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): - path_time = get_mtime(path) - if not path_time: - # assume directory doesn't exist now - continue - if abs(udir_time - path_time) < lock_timeout: - # assume directory with lockfile exists - # and lock timeout hasn't expired yet - continue - - # path dir locked for exclusive use - # and scheduled for removal to avoid another thread/process - # treating it as a new directory or removal candidate - garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) + # try acquiring lock to remove directory as exclusive user + if lock_timeout: + create_lockfile(path) + except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): + path_time = get_mtime(path) + if not path_time: + # assume directory doesn't exist now + continue + if abs(udir_time - path_time) < lock_timeout: + # assume directory with lockfile exists + # and lock timeout hasn't expired yet + continue + + # path dir locked for exclusive use + # and scheduled for removal to avoid another thread/process + # treating it as a new directory or removal candidate + garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) + try: + path.rename(garbage_path) + garbage_path.remove(rec=1) + except KeyboardInterrupt: + raise + except: # this might be py.error.Error, WindowsError ... + pass + if is_garbage(path): try: - path.rename(garbage_path) - garbage_path.remove(rec=1) - except KeyboardInterrupt: - raise - except: # this might be py.error.Error, WindowsError ... - pass - if is_garbage(path): - try: path.remove(rec=1) except KeyboardInterrupt: raise @@ -993,22 +993,22 @@ class LocalPath(FSBase): return udir make_numbered_dir = classmethod(make_numbered_dir) - + def copymode(src, dest): - """ copy permission from src to dst. """ - import shutil - shutil.copymode(src, dest) - - -def copystat(src, dest): - """ copy permission, last modification time, - last access time, and flags from src to dst.""" - import shutil - shutil.copystat(str(src), str(dest)) - - + """ copy permission from src to dst. """ + import shutil + shutil.copymode(src, dest) + + +def copystat(src, dest): + """ copy permission, last modification time, + last access time, and flags from src to dst.""" + import shutil + shutil.copystat(str(src), str(dest)) + + def copychunked(src, dest): - chunksize = 524288 # half a meg of bytes + chunksize = 524288 # half a meg of bytes fsrc = src.open('rb') try: fdest = dest.open('wb') @@ -1023,7 +1023,7 @@ def copychunked(src, dest): finally: fsrc.close() - + def isimportable(name): if name and (name[0].isalpha() or name[0] == '_'): name = name.replace("_", '') diff --git a/contrib/python/py/py/_path/svnurl.py b/contrib/python/py/py/_path/svnurl.py index 1ede990ed7..6589a71d09 100644 --- a/contrib/python/py/py/_path/svnurl.py +++ b/contrib/python/py/py/_path/svnurl.py @@ -315,7 +315,7 @@ class InfoSvnCommand: # locked, see 'svn help ls' lspattern = re.compile( r'^ *(?P<rev>\d+) +(?P<author>.+?) +(0? *(?P<size>\d+))? ' - r'*(?P<date>\w+ +\d{2} +[\d:]+) +(?P<file>.*)$') + r'*(?P<date>\w+ +\d{2} +[\d:]+) +(?P<file>.*)$') def __init__(self, line): # this is a typical line from 'svn ls http://...' #_ 1127 jum 0 Jul 13 15:28 branch/ diff --git a/contrib/python/py/py/_path/svnwc.py b/contrib/python/py/py/_path/svnwc.py index d69b51619a..b5b9d8d544 100644 --- a/contrib/python/py/py/_path/svnwc.py +++ b/contrib/python/py/py/_path/svnwc.py @@ -94,7 +94,7 @@ def _getsvnversion(ver=[]): def _escape_helper(text): text = str(text) - if sys.platform != 'win32': + if sys.platform != 'win32': text = str(text).replace('$', '\\$') return text @@ -327,7 +327,7 @@ def fixlocale(): return '' # some nasty chunk of code to solve path and url conversion and quoting issues -ILLEGAL_CHARS = '* | \\ / : < > ? \t \n \x0b \x0c \r'.split(' ') +ILLEGAL_CHARS = '* | \\ / : < > ? \t \n \x0b \x0c \r'.split(' ') if os.sep in ILLEGAL_CHARS: ILLEGAL_CHARS.remove(os.sep) ISWINDOWS = sys.platform == 'win32' @@ -354,7 +354,7 @@ def path_to_fspath(path, addat=True): def url_from_path(path): fspath = path_to_fspath(path, False) - from urllib import quote + from urllib import quote if ISWINDOWS: match = _reg_allow_disk.match(fspath) fspath = fspath.replace('\\', '/') @@ -490,7 +490,7 @@ class SvnWCCommandPath(common.PathBase): strerr.find('file already exists') != -1 or strerr.find('w150002:') != -1 or strerr.find("can't create directory") != -1): - raise py.error.EEXIST(strerr) #self) + raise py.error.EEXIST(strerr) #self) raise return out @@ -504,7 +504,7 @@ class SvnWCCommandPath(common.PathBase): if url is None: url = self.url if rev is None or rev == -1: - if (sys.platform != 'win32' and + if (sys.platform != 'win32' and _getsvnversion() == '1.3'): url += "@HEAD" else: @@ -785,7 +785,7 @@ recursively. """ info = InfoSvnWCCommand(output) # Can't reliably compare on Windows without access to win32api - if sys.platform != 'win32': + if sys.platform != 'win32': if info.path != self.localpath: raise py.error.ENOENT(self, "not a versioned resource:" + " %s != %s" % (info.path, self.localpath)) diff --git a/contrib/python/py/py/_std.py b/contrib/python/py/py/_std.py index bd84411da6..66adb7b023 100644 --- a/contrib/python/py/py/_std.py +++ b/contrib/python/py/py/_std.py @@ -1,11 +1,11 @@ import sys -import warnings +import warnings + + +class PyStdIsDeprecatedWarning(DeprecationWarning): + pass + - -class PyStdIsDeprecatedWarning(DeprecationWarning): - pass - - class Std(object): """ makes top-level python modules available as an attribute, importing them on first access. diff --git a/contrib/python/py/py/_vendored_packages/apipkg/__init__.py b/contrib/python/py/py/_vendored_packages/apipkg/__init__.py index 6f7cd75b39..350d8c4b07 100644 --- a/contrib/python/py/py/_vendored_packages/apipkg/__init__.py +++ b/contrib/python/py/py/_vendored_packages/apipkg/__init__.py @@ -1,50 +1,50 @@ -""" +""" apipkg: control the exported namespace of a Python package. - + see https://pypi.python.org/pypi/apipkg - -(c) holger krekel, 2009 - MIT license -""" -import os -import sys -from types import ModuleType - + +(c) holger krekel, 2009 - MIT license +""" +import os +import sys +from types import ModuleType + from .version import version as __version__ # NOQA:F401 - - -def _py_abspath(path): - """ - special version of abspath - that will leave paths from jython jars alone - """ + + +def _py_abspath(path): + """ + special version of abspath + that will leave paths from jython jars alone + """ if path.startswith("__pyclasspath__"): - - return path - else: - return os.path.abspath(path) - - -def distribution_version(name): - """try to get the version of the named distribution, - returs None on failure""" - from pkg_resources import get_distribution, DistributionNotFound - - try: - dist = get_distribution(name) - except DistributionNotFound: - pass - else: - return dist.version - - + + return path + else: + return os.path.abspath(path) + + +def distribution_version(name): + """try to get the version of the named distribution, + returs None on failure""" + from pkg_resources import get_distribution, DistributionNotFound + + try: + dist = get_distribution(name) + except DistributionNotFound: + pass + else: + return dist.version + + def initpkg(pkgname, exportdefs, attr=None, eager=False): - """ initialize given package from the export definitions. """ + """ initialize given package from the export definitions. """ attr = attr or {} - oldmod = sys.modules.get(pkgname) - d = {} + oldmod = sys.modules.get(pkgname) + d = {} f = getattr(oldmod, "__file__", None) - if f: - f = _py_abspath(f) + if f: + f = _py_abspath(f) d["__file__"] = f if hasattr(oldmod, "__version__"): d["__version__"] = oldmod.__version__ @@ -57,79 +57,79 @@ def initpkg(pkgname, exportdefs, attr=None, eager=False): if "__doc__" not in exportdefs and getattr(oldmod, "__doc__", None): d["__doc__"] = oldmod.__doc__ d["__spec__"] = getattr(oldmod, "__spec__", None) - d.update(attr) - if hasattr(oldmod, "__dict__"): - oldmod.__dict__.update(d) - mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) - sys.modules[pkgname] = mod - # eagerload in bypthon to avoid their monkeypatching breaking packages + d.update(attr) + if hasattr(oldmod, "__dict__"): + oldmod.__dict__.update(d) + mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) + sys.modules[pkgname] = mod + # eagerload in bypthon to avoid their monkeypatching breaking packages if "bpython" in sys.modules or eager: for module in list(sys.modules.values()): - if isinstance(module, ApiModule): - module.__dict__ + if isinstance(module, ApiModule): + module.__dict__ return mod - - -def importobj(modpath, attrname): + + +def importobj(modpath, attrname): """imports a module, then resolves the attrname on it""" module = __import__(modpath, None, None, ["__doc__"]) - if not attrname: - return module - - retval = module - names = attrname.split(".") - for x in names: - retval = getattr(retval, x) - return retval - - -class ApiModule(ModuleType): + if not attrname: + return module + + retval = module + names = attrname.split(".") + for x in names: + retval = getattr(retval, x) + return retval + + +class ApiModule(ModuleType): """the magical lazy-loading module standing""" - def __docget(self): - try: - return self.__doc - except AttributeError: + def __docget(self): + try: + return self.__doc + except AttributeError: if "__doc__" in self.__map__: return self.__makeattr("__doc__") - - def __docset(self, value): - self.__doc = value - - __doc__ = property(__docget, __docset) - - def __init__(self, name, importspec, implprefix=None, attr=None): - self.__name__ = name + + def __docset(self, value): + self.__doc = value + + __doc__ = property(__docget, __docset) + + def __init__(self, name, importspec, implprefix=None, attr=None): + self.__name__ = name self.__all__ = [x for x in importspec if x != "__onfirstaccess__"] - self.__map__ = {} - self.__implprefix__ = implprefix or name - if attr: - for name, val in attr.items(): - # print "setting", self.__name__, name, val - setattr(self, name, val) - for name, importspec in importspec.items(): - if isinstance(importspec, dict): + self.__map__ = {} + self.__implprefix__ = implprefix or name + if attr: + for name, val in attr.items(): + # print "setting", self.__name__, name, val + setattr(self, name, val) + for name, importspec in importspec.items(): + if isinstance(importspec, dict): subname = "{}.{}".format(self.__name__, name) - apimod = ApiModule(subname, importspec, implprefix) - sys.modules[subname] = apimod - setattr(self, name, apimod) - else: + apimod = ApiModule(subname, importspec, implprefix) + sys.modules[subname] = apimod + setattr(self, name, apimod) + else: parts = importspec.split(":") - modpath = parts.pop(0) - attrname = parts and parts[0] or "" + modpath = parts.pop(0) + attrname = parts and parts[0] or "" if modpath[0] == ".": - modpath = implprefix + modpath - - if not attrname: + modpath = implprefix + modpath + + if not attrname: subname = "{}.{}".format(self.__name__, name) - apimod = AliasModule(subname, modpath) - sys.modules[subname] = apimod + apimod = AliasModule(subname, modpath) + sys.modules[subname] = apimod if "." not in name: - setattr(self, name, apimod) - else: - self.__map__[name] = (modpath, attrname) - - def __repr__(self): + setattr(self, name, apimod) + else: + self.__map__[name] = (modpath, attrname) + + def __repr__(self): repr_list = [] if hasattr(self, "__version__"): repr_list.append("version=" + repr(self.__version__)) @@ -138,80 +138,80 @@ class ApiModule(ModuleType): if repr_list: return "<ApiModule {!r} {}>".format(self.__name__, " ".join(repr_list)) return "<ApiModule {!r}>".format(self.__name__) - - def __makeattr(self, name): - """lazily compute value for name or raise AttributeError if unknown.""" - # print "makeattr", self.__name__, name - target = None + + def __makeattr(self, name): + """lazily compute value for name or raise AttributeError if unknown.""" + # print "makeattr", self.__name__, name + target = None if "__onfirstaccess__" in self.__map__: target = self.__map__.pop("__onfirstaccess__") - importobj(*target)() - try: - modpath, attrname = self.__map__[name] - except KeyError: + importobj(*target)() + try: + modpath, attrname = self.__map__[name] + except KeyError: if target is not None and name != "__onfirstaccess__": - # retry, onfirstaccess might have set attrs - return getattr(self, name) - raise AttributeError(name) - else: - result = importobj(modpath, attrname) - setattr(self, name, result) - try: - del self.__map__[name] - except KeyError: - pass # in a recursive-import situation a double-del can happen - return result - - __getattr__ = __makeattr - - @property - def __dict__(self): - # force all the content of the module - # to be loaded when __dict__ is read + # retry, onfirstaccess might have set attrs + return getattr(self, name) + raise AttributeError(name) + else: + result = importobj(modpath, attrname) + setattr(self, name, result) + try: + del self.__map__[name] + except KeyError: + pass # in a recursive-import situation a double-del can happen + return result + + __getattr__ = __makeattr + + @property + def __dict__(self): + # force all the content of the module + # to be loaded when __dict__ is read dictdescr = ModuleType.__dict__["__dict__"] - dict = dictdescr.__get__(self) - if dict is not None: + dict = dictdescr.__get__(self) + if dict is not None: hasattr(self, "some") - for name in self.__all__: - try: - self.__makeattr(name) - except AttributeError: - pass - return dict - - -def AliasModule(modname, modpath, attrname=None): - mod = [] - - def getmod(): - if not mod: - x = importobj(modpath, None) - if attrname is not None: - x = getattr(x, attrname) - mod.append(x) - return mod[0] - + for name in self.__all__: + try: + self.__makeattr(name) + except AttributeError: + pass + return dict + + +def AliasModule(modname, modpath, attrname=None): + mod = [] + + def getmod(): + if not mod: + x = importobj(modpath, None) + if attrname is not None: + x = getattr(x, attrname) + mod.append(x) + return mod[0] + x = modpath + ("." + attrname if attrname else "") repr_result = "<AliasModule {!r} for {!r}>".format(modname, x) - class AliasModule(ModuleType): - def __repr__(self): + class AliasModule(ModuleType): + def __repr__(self): return repr_result - - def __getattribute__(self, name): - try: - return getattr(getmod(), name) - except ImportError: + + def __getattribute__(self, name): + try: + return getattr(getmod(), name) + except ImportError: if modpath == "pytest" and attrname is None: # hack for pylibs py.test return None else: raise - - def __setattr__(self, name, value): - setattr(getmod(), name, value) - - def __delattr__(self, name): - delattr(getmod(), name) - - return AliasModule(str(modname)) + + def __setattr__(self, name, value): + setattr(getmod(), name, value) + + def __delattr__(self, name): + delattr(getmod(), name) + + return AliasModule(str(modname)) diff --git a/contrib/python/py/py/_vendored_packages/iniconfig/__init__.py b/contrib/python/py/py/_vendored_packages/iniconfig/__init__.py index 9272bbde9a..ebef1fd720 100644 --- a/contrib/python/py/py/_vendored_packages/iniconfig/__init__.py +++ b/contrib/python/py/py/_vendored_packages/iniconfig/__init__.py @@ -1,56 +1,56 @@ -""" brain-dead simple parser for ini-style files. -(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed -""" +""" brain-dead simple parser for ini-style files. +(C) Ronny Pfannschmidt, Holger Krekel -- MIT licensed +""" import io -__all__ = ['IniConfig', 'ParseError'] - -COMMENTCHARS = "#;" - - -class ParseError(Exception): - def __init__(self, path, lineno, msg): - Exception.__init__(self, path, lineno, msg) - self.path = path - self.lineno = lineno - self.msg = msg - - def __str__(self): - return "%s:%s: %s" % (self.path, self.lineno+1, self.msg) - - -class SectionWrapper(object): - def __init__(self, config, name): - self.config = config - self.name = name - - def lineof(self, name): - return self.config.lineof(self.name, name) - - def get(self, key, default=None, convert=str): - return self.config.get(self.name, key, - convert=convert, default=default) - - def __getitem__(self, key): - return self.config.sections[self.name][key] - - def __iter__(self): - section = self.config.sections.get(self.name, []) - - def lineof(key): - return self.config.lineof(self.name, key) - for name in sorted(section, key=lineof): - yield name - - def items(self): - for name in self: - yield name, self[name] - - -class IniConfig(object): - def __init__(self, path, data=None): - self.path = str(path) # convenience - if data is None: +__all__ = ['IniConfig', 'ParseError'] + +COMMENTCHARS = "#;" + + +class ParseError(Exception): + def __init__(self, path, lineno, msg): + Exception.__init__(self, path, lineno, msg) + self.path = path + self.lineno = lineno + self.msg = msg + + def __str__(self): + return "%s:%s: %s" % (self.path, self.lineno+1, self.msg) + + +class SectionWrapper(object): + def __init__(self, config, name): + self.config = config + self.name = name + + def lineof(self, name): + return self.config.lineof(self.name, name) + + def get(self, key, default=None, convert=str): + return self.config.get(self.name, key, + convert=convert, default=default) + + def __getitem__(self, key): + return self.config.sections[self.name][key] + + def __iter__(self): + section = self.config.sections.get(self.name, []) + + def lineof(key): + return self.config.lineof(self.name, key) + for name in sorted(section, key=lineof): + yield name + + def items(self): + for name in self: + yield name, self[name] + + +class IniConfig(object): + def __init__(self, path, data=None): + self.path = str(path) # convenience + if data is None: if self.path.startswith('pkg:'): import pkgutil @@ -59,116 +59,116 @@ class IniConfig(object): f = io.StringIO(content.decode('utf-8')) else: f = open(self.path) - try: - tokens = self._parse(iter(f)) - finally: - f.close() - else: - tokens = self._parse(data.splitlines(True)) - - self._sources = {} - self.sections = {} - - for lineno, section, name, value in tokens: - if section is None: - self._raise(lineno, 'no section header defined') - self._sources[section, name] = lineno - if name is None: - if section in self.sections: - self._raise(lineno, 'duplicate section %r' % (section, )) - self.sections[section] = {} - else: - if name in self.sections[section]: - self._raise(lineno, 'duplicate name %r' % (name, )) - self.sections[section][name] = value - - def _raise(self, lineno, msg): - raise ParseError(self.path, lineno, msg) - - def _parse(self, line_iter): - result = [] - section = None - for lineno, line in enumerate(line_iter): - name, data = self._parseline(line, lineno) - # new value - if name is not None and data is not None: - result.append((lineno, section, name, data)) - # new section - elif name is not None and data is None: - if not name: - self._raise(lineno, 'empty section name') - section = name - result.append((lineno, section, None, None)) - # continuation - elif name is None and data is not None: - if not result: - self._raise(lineno, 'unexpected value continuation') - last = result.pop() - last_name, last_data = last[-2:] - if last_name is None: - self._raise(lineno, 'unexpected value continuation') - - if last_data: - data = '%s\n%s' % (last_data, data) - result.append(last[:-1] + (data,)) - return result - - def _parseline(self, line, lineno): - # blank lines - if iscommentline(line): - line = "" - else: - line = line.rstrip() - if not line: - return None, None - # section - if line[0] == '[': - realline = line - for c in COMMENTCHARS: - line = line.split(c)[0].rstrip() - if line[-1] == "]": - return line[1:-1], None - return None, realline.strip() - # value - elif not line[0].isspace(): - try: - name, value = line.split('=', 1) - if ":" in name: - raise ValueError() - except ValueError: - try: - name, value = line.split(":", 1) - except ValueError: - self._raise(lineno, 'unexpected line: %r' % line) - return name.strip(), value.strip() - # continuation - else: - return None, line.strip() - - def lineof(self, section, name=None): - lineno = self._sources.get((section, name)) - if lineno is not None: - return lineno + 1 - - def get(self, section, name, default=None, convert=str): - try: - return convert(self.sections[section][name]) - except KeyError: - return default - - def __getitem__(self, name): - if name not in self.sections: - raise KeyError(name) - return SectionWrapper(self, name) - - def __iter__(self): - for name in sorted(self.sections, key=self.lineof): - yield SectionWrapper(self, name) - - def __contains__(self, arg): - return arg in self.sections - - -def iscommentline(line): - c = line.lstrip()[:1] - return c in COMMENTCHARS + try: + tokens = self._parse(iter(f)) + finally: + f.close() + else: + tokens = self._parse(data.splitlines(True)) + + self._sources = {} + self.sections = {} + + for lineno, section, name, value in tokens: + if section is None: + self._raise(lineno, 'no section header defined') + self._sources[section, name] = lineno + if name is None: + if section in self.sections: + self._raise(lineno, 'duplicate section %r' % (section, )) + self.sections[section] = {} + else: + if name in self.sections[section]: + self._raise(lineno, 'duplicate name %r' % (name, )) + self.sections[section][name] = value + + def _raise(self, lineno, msg): + raise ParseError(self.path, lineno, msg) + + def _parse(self, line_iter): + result = [] + section = None + for lineno, line in enumerate(line_iter): + name, data = self._parseline(line, lineno) + # new value + if name is not None and data is not None: + result.append((lineno, section, name, data)) + # new section + elif name is not None and data is None: + if not name: + self._raise(lineno, 'empty section name') + section = name + result.append((lineno, section, None, None)) + # continuation + elif name is None and data is not None: + if not result: + self._raise(lineno, 'unexpected value continuation') + last = result.pop() + last_name, last_data = last[-2:] + if last_name is None: + self._raise(lineno, 'unexpected value continuation') + + if last_data: + data = '%s\n%s' % (last_data, data) + result.append(last[:-1] + (data,)) + return result + + def _parseline(self, line, lineno): + # blank lines + if iscommentline(line): + line = "" + else: + line = line.rstrip() + if not line: + return None, None + # section + if line[0] == '[': + realline = line + for c in COMMENTCHARS: + line = line.split(c)[0].rstrip() + if line[-1] == "]": + return line[1:-1], None + return None, realline.strip() + # value + elif not line[0].isspace(): + try: + name, value = line.split('=', 1) + if ":" in name: + raise ValueError() + except ValueError: + try: + name, value = line.split(":", 1) + except ValueError: + self._raise(lineno, 'unexpected line: %r' % line) + return name.strip(), value.strip() + # continuation + else: + return None, line.strip() + + def lineof(self, section, name=None): + lineno = self._sources.get((section, name)) + if lineno is not None: + return lineno + 1 + + def get(self, section, name, default=None, convert=str): + try: + return convert(self.sections[section][name]) + except KeyError: + return default + + def __getitem__(self, name): + if name not in self.sections: + raise KeyError(name) + return SectionWrapper(self, name) + + def __iter__(self): + for name in sorted(self.sections, key=self.lineof): + yield SectionWrapper(self, name) + + def __contains__(self, arg): + return arg in self.sections + + +def iscommentline(line): + c = line.lstrip()[:1] + return c in COMMENTCHARS diff --git a/contrib/python/py/py/_version.py b/contrib/python/py/py/_version.py index 8e00d52a10..3d30fbec42 100644 --- a/contrib/python/py/py/_version.py +++ b/contrib/python/py/py/_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 = '1.11.0' version_tuple = (1, 11, 0) diff --git a/contrib/python/py/py/_xmlgen.py b/contrib/python/py/py/_xmlgen.py index 0831a9926e..1c83545884 100644 --- a/contrib/python/py/py/_xmlgen.py +++ b/contrib/python/py/py/_xmlgen.py @@ -74,18 +74,18 @@ class html(Namespace): __tagclass__ = HtmlTag __stickyname__ = True __tagspec__ = dict([(x,1) for x in ( - 'a,abbr,acronym,address,applet,area,article,aside,audio,b,' - 'base,basefont,bdi,bdo,big,blink,blockquote,body,br,button,' - 'canvas,caption,center,cite,code,col,colgroup,command,comment,' - 'datalist,dd,del,details,dfn,dir,div,dl,dt,em,embed,' - 'fieldset,figcaption,figure,footer,font,form,frame,frameset,h1,' - 'h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,img,input,' - 'ins,isindex,kbd,keygen,label,legend,li,link,listing,map,mark,' - 'marquee,menu,meta,meter,multicol,nav,nobr,noembed,noframes,' - 'noscript,object,ol,optgroup,option,output,p,param,pre,progress,' - 'q,rp,rt,ruby,s,samp,script,section,select,small,source,span,' - 'strike,strong,style,sub,summary,sup,table,tbody,td,textarea,' - 'tfoot,th,thead,time,title,tr,track,tt,u,ul,xmp,var,video,wbr' + 'a,abbr,acronym,address,applet,area,article,aside,audio,b,' + 'base,basefont,bdi,bdo,big,blink,blockquote,body,br,button,' + 'canvas,caption,center,cite,code,col,colgroup,command,comment,' + 'datalist,dd,del,details,dfn,dir,div,dl,dt,em,embed,' + 'fieldset,figcaption,figure,footer,font,form,frame,frameset,h1,' + 'h2,h3,h4,h5,h6,head,header,hgroup,hr,html,i,iframe,img,input,' + 'ins,isindex,kbd,keygen,label,legend,li,link,listing,map,mark,' + 'marquee,menu,meta,meter,multicol,nav,nobr,noembed,noframes,' + 'noscript,object,ol,optgroup,option,output,p,param,pre,progress,' + 'q,rp,rt,ruby,s,samp,script,section,select,small,source,span,' + 'strike,strong,style,sub,summary,sup,table,tbody,td,textarea,' + 'tfoot,th,thead,time,title,tr,track,tt,u,ul,xmp,var,video,wbr' ).split(',') if x]) class Style(object): diff --git a/contrib/python/py/ya.make b/contrib/python/py/ya.make index 870b852488..cc86cb7fa9 100644 --- a/contrib/python/py/ya.make +++ b/contrib/python/py/ya.make @@ -30,7 +30,7 @@ PY_SRCS( py/_code/source.py py/_error.py py/_io/__init__.py - py/_io/capture.py + py/_io/capture.py py/_io/saferepr.py py/_io/terminalwriter.py py/_log/__init__.py @@ -47,12 +47,12 @@ PY_SRCS( py/_process/forkedfunc.py py/_process/killproc.py py/_std.py - py/_vendored_packages/__init__.py + py/_vendored_packages/__init__.py py/_vendored_packages/apipkg/__init__.py py/_vendored_packages/apipkg/version.py py/_vendored_packages/iniconfig/__init__.py py/_vendored_packages/iniconfig/__init__.pyi - py/_version.py + py/_version.py py/_xmlgen.py py/error.pyi py/iniconfig.pyi diff --git a/contrib/python/pytest/py2/LICENSE b/contrib/python/pytest/py2/LICENSE index 958fc1d1c6..d14fb7ff4b 100644 --- a/contrib/python/pytest/py2/LICENSE +++ b/contrib/python/pytest/py2/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/py2/_pytest/__init__.py b/contrib/python/pytest/py2/_pytest/__init__.py index 42eb1578e2..17cc20b615 100644 --- a/contrib/python/pytest/py2/_pytest/__init__.py +++ b/contrib/python/pytest/py2/_pytest/__init__.py @@ -1,9 +1,9 @@ # -*- coding: utf-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/py2/_pytest/_argcomplete.py b/contrib/python/pytest/py2/_pytest/_argcomplete.py index ba7bc89b70..c6cf1d8fdd 100644 --- a/contrib/python/pytest/py2/_pytest/_argcomplete.py +++ b/contrib/python/pytest/py2/_pytest/_argcomplete.py @@ -1,110 +1,110 @@ # -*- coding: utf-8 -*- -"""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 -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 ": - - 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 -(/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 -so the the python-argcomplete-check-easy-install-script does not -need to be called to find the entry point of the code and see if that is -marked with PYTHON_ARGCOMPLETE_OK - -INSTALL/DEBUGGING -================= -To include this support in another application that has setup.py generated -scripts: -- add the line: - # 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 - , call try_argcomplete just before parse_args(), and optionally add - filescompleter to the positional arguments' add_argument() -If things do not work right away: -- switch on argcomplete debugging with (also helpful when doing custom - completers): - export _ARC_DEBUG=1 -- run: - 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). -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os -import sys -from glob import glob - - -class FastFilesCompleter(object): - "Fast file completer class" - - def __init__(self, directories=True): - self.directories = directories - - def __call__(self, prefix, **kwargs): - """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: - # 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 += "/" - # 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) - filescompleter = FastFilesCompleter() - - def try_argcomplete(parser): - argcomplete.autocomplete(parser, always_complete_options=False) - - -else: - - def try_argcomplete(parser): - pass - - filescompleter = None +"""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 +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 ": + + 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 +(/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 +so the the python-argcomplete-check-easy-install-script does not +need to be called to find the entry point of the code and see if that is +marked with PYTHON_ARGCOMPLETE_OK + +INSTALL/DEBUGGING +================= +To include this support in another application that has setup.py generated +scripts: +- add the line: + # 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 + , call try_argcomplete just before parse_args(), and optionally add + filescompleter to the positional arguments' add_argument() +If things do not work right away: +- switch on argcomplete debugging with (also helpful when doing custom + completers): + export _ARC_DEBUG=1 +- run: + 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). +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +from glob import glob + + +class FastFilesCompleter(object): + "Fast file completer class" + + def __init__(self, directories=True): + self.directories = directories + + def __call__(self, prefix, **kwargs): + """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: + # 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 += "/" + # 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) + filescompleter = FastFilesCompleter() + + def try_argcomplete(parser): + argcomplete.autocomplete(parser, always_complete_options=False) + + +else: + + def try_argcomplete(parser): + pass + + filescompleter = None diff --git a/contrib/python/pytest/py2/_pytest/_code/__init__.py b/contrib/python/pytest/py2/_pytest/_code/__init__.py index 654aaac4b0..1394b2b10e 100644 --- a/contrib/python/pytest/py2/_pytest/_code/__init__.py +++ b/contrib/python/pytest/py2/_pytest/_code/__init__.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -""" python inspection/code generation API """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from .code import Code # noqa -from .code import ExceptionInfo # noqa -from .code import filter_traceback # noqa -from .code import Frame # noqa -from .code import getrawcode # noqa -from .code import Traceback # noqa -from .source import compile_ as compile # noqa -from .source import getfslineno # noqa -from .source import Source # noqa +""" python inspection/code generation API """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from .code import Code # noqa +from .code import ExceptionInfo # noqa +from .code import filter_traceback # noqa +from .code import Frame # noqa +from .code import getrawcode # noqa +from .code import Traceback # noqa +from .source import compile_ as compile # noqa +from .source import getfslineno # noqa +from .source import Source # noqa diff --git a/contrib/python/pytest/py2/_pytest/_code/_py2traceback.py b/contrib/python/pytest/py2/_pytest/_code/_py2traceback.py index 7697d9c502..faacc02166 100644 --- a/contrib/python/pytest/py2/_pytest/_code/_py2traceback.py +++ b/contrib/python/pytest/py2/_pytest/_code/_py2traceback.py @@ -1,95 +1,95 @@ # -*- coding: utf-8 -*- -# copied from python-2.7.3's traceback.py -# CHANGES: -# - some_str is replaced, trying to create unicode strings -# -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -import types - -from six import text_type - - -def format_exception_only(etype, value): - """Format the exception part of a traceback. - - The arguments are the exception type and value such as given by - sys.last_type and sys.last_value. The return value is a list of - strings, each ending in a newline. - - Normally, the list contains a single string; however, for - SyntaxError exceptions, it contains several lines that (when - printed) display detailed information about where the syntax - error occurred. - - The message indicating which exception occurred is always the last - string in the list. - - """ - - # An instance should not have a meaningful value parameter, but - # sometimes does, particularly for string exceptions, such as - # >>> raise string1, string2 # deprecated - # - # Clear these out first because issubtype(string1, SyntaxError) - # would throw another exception and mask the original problem. - if ( - isinstance(etype, BaseException) - or isinstance(etype, types.InstanceType) - or etype is None - or type(etype) is str - ): - return [_format_final_exc_line(etype, value)] - - stype = etype.__name__ - - if not issubclass(etype, SyntaxError): - return [_format_final_exc_line(stype, value)] - - # It was a syntax error; show exactly where the problem was found. - lines = [] - try: - msg, (filename, lineno, offset, badline) = value.args - except Exception: - pass - else: - filename = filename or "<string>" - lines.append(' File "{}", line {}\n'.format(filename, lineno)) - if badline is not None: - if isinstance(badline, bytes): # python 2 only - badline = badline.decode("utf-8", "replace") - lines.append(" {}\n".format(badline.strip())) - if offset is not None: - caretspace = badline.rstrip("\n")[:offset].lstrip() - # non-space whitespace (likes tabs) must be kept for alignment - caretspace = ((c.isspace() and c or " ") for c in caretspace) - # only three spaces to account for offset1 == pos 0 - lines.append(" {}^\n".format("".join(caretspace))) - value = msg - - lines.append(_format_final_exc_line(stype, value)) - return lines - - -def _format_final_exc_line(etype, value): - """Return a list of a single line -- normal case for format_exception_only""" - valuestr = _some_str(value) - if value is None or not valuestr: - line = "{}\n".format(etype) - else: - line = "{}: {}\n".format(etype, valuestr) - return line - - -def _some_str(value): - try: - return text_type(value) - except Exception: - try: - return bytes(value).decode("UTF-8", "replace") - except Exception: - pass - return "<unprintable {} object>".format(type(value).__name__) +# copied from python-2.7.3's traceback.py +# CHANGES: +# - some_str is replaced, trying to create unicode strings +# +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import types + +from six import text_type + + +def format_exception_only(etype, value): + """Format the exception part of a traceback. + + The arguments are the exception type and value such as given by + sys.last_type and sys.last_value. The return value is a list of + strings, each ending in a newline. + + Normally, the list contains a single string; however, for + SyntaxError exceptions, it contains several lines that (when + printed) display detailed information about where the syntax + error occurred. + + The message indicating which exception occurred is always the last + string in the list. + + """ + + # An instance should not have a meaningful value parameter, but + # sometimes does, particularly for string exceptions, such as + # >>> raise string1, string2 # deprecated + # + # Clear these out first because issubtype(string1, SyntaxError) + # would throw another exception and mask the original problem. + if ( + isinstance(etype, BaseException) + or isinstance(etype, types.InstanceType) + or etype is None + or type(etype) is str + ): + return [_format_final_exc_line(etype, value)] + + stype = etype.__name__ + + if not issubclass(etype, SyntaxError): + return [_format_final_exc_line(stype, value)] + + # It was a syntax error; show exactly where the problem was found. + lines = [] + try: + msg, (filename, lineno, offset, badline) = value.args + except Exception: + pass + else: + filename = filename or "<string>" + lines.append(' File "{}", line {}\n'.format(filename, lineno)) + if badline is not None: + if isinstance(badline, bytes): # python 2 only + badline = badline.decode("utf-8", "replace") + lines.append(" {}\n".format(badline.strip())) + if offset is not None: + caretspace = badline.rstrip("\n")[:offset].lstrip() + # non-space whitespace (likes tabs) must be kept for alignment + caretspace = ((c.isspace() and c or " ") for c in caretspace) + # only three spaces to account for offset1 == pos 0 + lines.append(" {}^\n".format("".join(caretspace))) + value = msg + + lines.append(_format_final_exc_line(stype, value)) + return lines + + +def _format_final_exc_line(etype, value): + """Return a list of a single line -- normal case for format_exception_only""" + valuestr = _some_str(value) + if value is None or not valuestr: + line = "{}\n".format(etype) + else: + line = "{}: {}\n".format(etype, valuestr) + return line + + +def _some_str(value): + try: + return text_type(value) + except Exception: + try: + return bytes(value).decode("UTF-8", "replace") + except Exception: + pass + return "<unprintable {} object>".format(type(value).__name__) diff --git a/contrib/python/pytest/py2/_pytest/_code/code.py b/contrib/python/pytest/py2/_pytest/_code/code.py index b8ebe7a1d3..175d6fda01 100644 --- a/contrib/python/pytest/py2/_pytest/_code/code.py +++ b/contrib/python/pytest/py2/_pytest/_code/code.py @@ -1,408 +1,408 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import inspect -import re -import sys -import traceback -from inspect import CO_VARARGS -from inspect import CO_VARKEYWORDS -from weakref import ref - -import attr -import pluggy -import py -from six import text_type - -import _pytest +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import inspect +import re +import sys +import traceback +from inspect import CO_VARARGS +from inspect import CO_VARKEYWORDS +from weakref import ref + +import attr +import pluggy +import py +from six import text_type + +import _pytest from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr -from _pytest.compat import _PY2 -from _pytest.compat import _PY3 -from _pytest.compat import PY35 -from _pytest.compat import safe_str - -if _PY3: - from traceback import format_exception_only -else: - from ._py2traceback import format_exception_only - - -class Code(object): - """ wrapper around Python code objects """ - - def __init__(self, rawcode): - if not hasattr(rawcode, "co_filename"): - rawcode = getrawcode(rawcode) - try: - self.filename = rawcode.co_filename - self.firstlineno = rawcode.co_firstlineno - 1 - self.name = rawcode.co_name - except AttributeError: - raise TypeError("not a code object: %r" % (rawcode,)) - self.raw = rawcode - - def __eq__(self, other): - return self.raw == other.raw - - __hash__ = None - - def __ne__(self, other): - return not self == other - - @property - def path(self): - """ return a path object pointing to source code (note that it - might not point to an actually existing file). """ - 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.") - except OSError: - # XXX maybe try harder like the weird logic - # in the standard lib [linecache.updatecache] does? - p = self.raw.co_filename - - return p - - @property - def fullsource(self): - """ return a _pytest._code.Source object for the full source file of the code - """ - from _pytest._code import source - - full, _ = source.findsource(self.raw) - return full - - def source(self): - """ return a _pytest._code.Source object for the code object's source only - """ - # return source only for that part of code - import _pytest._code - - return _pytest._code.Source(self.raw) - - def getargs(self, var=False): - """ 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 - """ - # handfull 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] - - -class Frame(object): - """Wrapper around a Python frame holding f_locals and f_globals - in which expressions can be evaluated.""" - - def __init__(self, frame): - self.lineno = frame.f_lineno - 1 - self.f_globals = frame.f_globals - self.f_locals = frame.f_locals - self.raw = frame - self.code = Code(frame.f_code) - - @property - def statement(self): - """ statement this frame is at """ - import _pytest._code - - if self.code.fullsource is None: - return _pytest._code.Source("") - 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) - - def exec_(self, code, **vars): - """ exec 'code' in the frame - - 'vars' are optiona; additional local variables - """ - f_locals = self.f_locals.copy() - f_locals.update(vars) +from _pytest.compat import _PY2 +from _pytest.compat import _PY3 +from _pytest.compat import PY35 +from _pytest.compat import safe_str + +if _PY3: + from traceback import format_exception_only +else: + from ._py2traceback import format_exception_only + + +class Code(object): + """ wrapper around Python code objects """ + + def __init__(self, rawcode): + if not hasattr(rawcode, "co_filename"): + rawcode = getrawcode(rawcode) + try: + self.filename = rawcode.co_filename + self.firstlineno = rawcode.co_firstlineno - 1 + self.name = rawcode.co_name + except AttributeError: + raise TypeError("not a code object: %r" % (rawcode,)) + self.raw = rawcode + + def __eq__(self, other): + return self.raw == other.raw + + __hash__ = None + + def __ne__(self, other): + return not self == other + + @property + def path(self): + """ return a path object pointing to source code (note that it + might not point to an actually existing file). """ + 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.") + except OSError: + # XXX maybe try harder like the weird logic + # in the standard lib [linecache.updatecache] does? + p = self.raw.co_filename + + return p + + @property + def fullsource(self): + """ return a _pytest._code.Source object for the full source file of the code + """ + from _pytest._code import source + + full, _ = source.findsource(self.raw) + return full + + def source(self): + """ return a _pytest._code.Source object for the code object's source only + """ + # return source only for that part of code + import _pytest._code + + return _pytest._code.Source(self.raw) + + def getargs(self, var=False): + """ 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 + """ + # handfull 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] + + +class Frame(object): + """Wrapper around a Python frame holding f_locals and f_globals + in which expressions can be evaluated.""" + + def __init__(self, frame): + self.lineno = frame.f_lineno - 1 + self.f_globals = frame.f_globals + self.f_locals = frame.f_locals + self.raw = frame + self.code = Code(frame.f_code) + + @property + def statement(self): + """ statement this frame is at """ + import _pytest._code + + if self.code.fullsource is None: + return _pytest._code.Source("") + 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) + + def exec_(self, code, **vars): + """ exec 'code' in the frame + + 'vars' are optiona; additional local variables + """ + f_locals = self.f_locals.copy() + f_locals.update(vars) exec(code, self.f_globals, f_locals) - - def repr(self, object): - """ return a 'safe' (non-recursive, one-line) string repr for 'object' - """ + + def repr(self, object): + """ return a 'safe' (non-recursive, one-line) string repr for 'object' + """ return saferepr(object) - - def is_true(self, object): - return object - - def getargs(self, var=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 - - -class TracebackEntry(object): - """ a single entry in a traceback """ - - _repr_style = None - exprinfo = None - - def __init__(self, rawentry, excinfo=None): - self._excinfo = excinfo - self._rawentry = rawentry - self.lineno = rawentry.tb_lineno - 1 - - def set_repr_style(self, mode): - assert mode in ("short", "long") - self._repr_style = mode - - @property - def frame(self): - import _pytest._code - - return _pytest._code.Frame(self._rawentry.tb_frame) - - @property - def relline(self): - return self.lineno - self.frame.code.firstlineno - - def __repr__(self): - return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1) - - @property - def statement(self): - """ _pytest._code.Source object for the current statement """ - source = self.frame.code.fullsource - return source.getstatement(self.lineno) - - @property - def path(self): - """ path to the source code """ - return self.frame.code.path - - def getlocals(self): - return self.frame.f_locals - - locals = property(getlocals, None, None, "locals of underlaying frame") - - def getfirstlinesource(self): - # on Jython this firstlineno can be -1 apparently - return max(self.frame.code.firstlineno, 0) - - def getsource(self, astcache=None): - """ return failing source code. """ - # we use the passed in astcache to not reparse asttrees - # within exception info printing - from _pytest._code.source import getstatementrange_ast - - 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): - """ return True if the current frame has a var __tracebackhide__ + + def is_true(self, object): + return object + + def getargs(self, var=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 + + +class TracebackEntry(object): + """ a single entry in a traceback """ + + _repr_style = None + exprinfo = None + + def __init__(self, rawentry, excinfo=None): + self._excinfo = excinfo + self._rawentry = rawentry + self.lineno = rawentry.tb_lineno - 1 + + def set_repr_style(self, mode): + assert mode in ("short", "long") + self._repr_style = mode + + @property + def frame(self): + import _pytest._code + + return _pytest._code.Frame(self._rawentry.tb_frame) + + @property + def relline(self): + return self.lineno - self.frame.code.firstlineno + + def __repr__(self): + return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1) + + @property + def statement(self): + """ _pytest._code.Source object for the current statement """ + source = self.frame.code.fullsource + return source.getstatement(self.lineno) + + @property + def path(self): + """ path to the source code """ + return self.frame.code.path + + def getlocals(self): + return self.frame.f_locals + + locals = property(getlocals, None, None, "locals of underlaying frame") + + def getfirstlinesource(self): + # on Jython this firstlineno can be -1 apparently + return max(self.frame.code.firstlineno, 0) + + def getsource(self, astcache=None): + """ return failing source code. """ + # we use the passed in astcache to not reparse asttrees + # within exception info printing + from _pytest._code.source import getstatementrange_ast + + 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): + """ 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 - """ + + If __tracebackhide__ is a callable, it gets called with the + ExceptionInfo instance and can decide whether to hide the traceback. + + mostly for internal use + """ f = self.frame tbh = f.f_locals.get( "__tracebackhide__", f.f_globals.get("__tracebackhide__", False) ) 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): - try: - fn = str(self.path) - except py.error.Error: - fn = "???" - name = self.frame.code.name - try: - line = str(self.statement).lstrip() - except KeyboardInterrupt: - raise - except: # noqa - line = "???" - return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) - - def name(self): - return self.frame.code.raw.co_name - - name = property(name, None, None, "co_name of underlaying code") - - -class Traceback(list): - """ Traceback objects encapsulate and offer higher level - access to Traceback entries. - """ - - Entry = TracebackEntry - - def __init__(self, tb, excinfo=None): - """ initialize from given python traceback object and ExceptionInfo """ - self._excinfo = excinfo - if hasattr(tb, "tb_next"): - - def f(cur): - while cur is not None: - yield self.Entry(cur, excinfo=excinfo) - cur = cur.tb_next - - list.__init__(self, f(tb)) - else: - list.__init__(self, tb) - - def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): - """ return a Traceback instance wrapping part of this Traceback - - by provding 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 - or not hasattr(codepath, "relto") - 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 - - def __getitem__(self, key): - val = super(Traceback, self).__getitem__(key) - if isinstance(key, type(slice(0))): - val = self.__class__(val) - return val - - def filter(self, fn=lambda x: not x.ishidden()): - """ 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) - - def getcrashentry(self): - """ 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] - - def recursionindex(self): - """ return the index of the frame/TracebackEntry where recursion - originates if appropriate, None if no recursion occurred - """ - cache = {} - 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.is_true( - 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" -) - - + + def __str__(self): + try: + fn = str(self.path) + except py.error.Error: + fn = "???" + name = self.frame.code.name + try: + line = str(self.statement).lstrip() + except KeyboardInterrupt: + raise + except: # noqa + line = "???" + return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) + + def name(self): + return self.frame.code.raw.co_name + + name = property(name, None, None, "co_name of underlaying code") + + +class Traceback(list): + """ Traceback objects encapsulate and offer higher level + access to Traceback entries. + """ + + Entry = TracebackEntry + + def __init__(self, tb, excinfo=None): + """ initialize from given python traceback object and ExceptionInfo """ + self._excinfo = excinfo + if hasattr(tb, "tb_next"): + + def f(cur): + while cur is not None: + yield self.Entry(cur, excinfo=excinfo) + cur = cur.tb_next + + list.__init__(self, f(tb)) + else: + list.__init__(self, tb) + + def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): + """ return a Traceback instance wrapping part of this Traceback + + by provding 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 + or not hasattr(codepath, "relto") + 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 + + def __getitem__(self, key): + val = super(Traceback, self).__getitem__(key) + if isinstance(key, type(slice(0))): + val = self.__class__(val) + return val + + def filter(self, fn=lambda x: not x.ishidden()): + """ 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) + + def getcrashentry(self): + """ 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] + + def recursionindex(self): + """ return the index of the frame/TracebackEntry where recursion + originates if appropriate, None if no recursion occurred + """ + cache = {} + 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.is_true( + 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" +) + + @attr.s(repr=False) -class ExceptionInfo(object): - """ wraps sys.exc_info() objects and offers - help for navigating the traceback. - """ - - _assert_start_repr = ( - "AssertionError(u'assert " if _PY2 else "AssertionError('assert " - ) - +class ExceptionInfo(object): + """ wraps sys.exc_info() objects and offers + help for navigating the traceback. + """ + + _assert_start_repr = ( + "AssertionError(u'assert " if _PY2 else "AssertionError('assert " + ) + _excinfo = attr.ib() _striptext = attr.ib(default="") _traceback = attr.ib(default=None) - + @classmethod def from_current(cls, exprinfo=None): """returns an ExceptionInfo matching the current traceback - + .. warning:: Experimental API @@ -461,117 +461,117 @@ class ExceptionInfo(object): def traceback(self, value): self._traceback = value - def __repr__(self): + def __repr__(self): if self._excinfo is None: return "<ExceptionInfo for raises contextmanager>" return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback)) - - def exconly(self, tryshort=False): - """ 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 - - def errisinstance(self, exc): - """ return True if the exception is an instance of exc """ - return isinstance(self.value, exc) - - def _getreprcrash(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=False, - style="long", - abspath=False, - tbfilter=True, - funcargs=False, - truncate_locals=True, - chain=True, - ): - """ - Return str()able representation of this exception info. - - :param bool showlocals: - Show locals per traceback entry. - Ignored if ``style=="native"``. - - :param str style: long|short|no|native 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 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) - - def __str__(self): + + def exconly(self, tryshort=False): + """ 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 + + def errisinstance(self, exc): + """ return True if the exception is an instance of exc """ + return isinstance(self.value, exc) + + def _getreprcrash(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=False, + style="long", + abspath=False, + tbfilter=True, + funcargs=False, + truncate_locals=True, + chain=True, + ): + """ + Return str()able representation of this exception info. + + :param bool showlocals: + Show locals per traceback entry. + Ignored if ``style=="native"``. + + :param str style: long|short|no|native 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 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) + + def __str__(self): if self._excinfo is None: - return repr(self) + return repr(self) entry = self.traceback[-1] loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) return str(loc) - - def __unicode__(self): - entry = self.traceback[-1] - loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) - return text_type(loc) - - def match(self, regexp): - """ + + def __unicode__(self): + entry = self.traceback[-1] + loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) + return text_type(loc) + + def match(self, regexp): + """ Check whether the regular expression 'regexp' is found in the string representation of the exception using ``re.search``. If it matches then True is returned (so that it is possible to write ``assert excinfo.match()``). If it doesn't match an AssertionError is raised. - """ - __tracebackhide__ = True + """ + __tracebackhide__ = True value = ( text_type(self.value) if isinstance(regexp, text_type) else str(self.value) ) @@ -579,515 +579,515 @@ class ExceptionInfo(object): raise AssertionError( u"Pattern {!r} not found in {!r}".format(regexp, value) ) - return True - - -@attr.s -class FormattedExcinfo(object): - """ presenting information about failing Functions and Generators. """ - - # for traceback entries - flow_marker = ">" - fail_marker = "E" - - showlocals = attr.ib(default=False) - style = attr.ib(default="long") - abspath = attr.ib(default=True) - tbfilter = attr.ib(default=True) - funcargs = attr.ib(default=False) - truncate_locals = attr.ib(default=True) - chain = attr.ib(default=True) - astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) - - def _getindent(self, source): - # figure out indent for given source - try: - s = str(source.getstatement(len(source) - 1)) - except KeyboardInterrupt: - raise - except: # noqa - try: - s = str(source[-1]) - except KeyboardInterrupt: - raise - except: # noqa - return 0 - return 4 + (len(s) - len(s.lstrip())) - - def _getentrysource(self, entry): - source = entry.getsource(self.astcache) - if source is not None: - source = source.deindent() - return source - - def repr_args(self, entry): - if self.funcargs: - args = [] - for argname, argvalue in entry.frame.getargs(var=True): + return True + + +@attr.s +class FormattedExcinfo(object): + """ presenting information about failing Functions and Generators. """ + + # for traceback entries + flow_marker = ">" + fail_marker = "E" + + showlocals = attr.ib(default=False) + style = attr.ib(default="long") + abspath = attr.ib(default=True) + tbfilter = attr.ib(default=True) + funcargs = attr.ib(default=False) + truncate_locals = attr.ib(default=True) + chain = attr.ib(default=True) + astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) + + def _getindent(self, source): + # figure out indent for given source + try: + s = str(source.getstatement(len(source) - 1)) + except KeyboardInterrupt: + raise + except: # noqa + try: + s = str(source[-1]) + except KeyboardInterrupt: + raise + except: # noqa + return 0 + return 4 + (len(s) - len(s.lstrip())) + + def _getentrysource(self, entry): + source = entry.getsource(self.astcache) + if source is not None: + source = source.deindent() + return source + + def repr_args(self, entry): + if self.funcargs: + args = [] + for argname, argvalue in entry.frame.getargs(var=True): args.append((argname, saferepr(argvalue))) - return ReprFuncArgs(args) - - def get_source(self, source, line_index=-1, excinfo=None, short=False): - """ return formatted and marked up source lines. """ - import _pytest._code - - lines = [] - if source is None or line_index >= len(source.lines): - source = _pytest._code.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 - - def get_exconly(self, excinfo, indent=4, markall=False): - lines = [] - indent = " " * indent - # get the real exception information out - exlines = excinfo.exconly(tryshort=True).split("\n") - failindent = self.fail_marker + indent[1:] - for line in exlines: - lines.append(failindent + line) - if not markall: - failindent = indent - return lines - - def repr_locals(self, 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: + return ReprFuncArgs(args) + + def get_source(self, source, line_index=-1, excinfo=None, short=False): + """ return formatted and marked up source lines. """ + import _pytest._code + + lines = [] + if source is None or line_index >= len(source.lines): + source = _pytest._code.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 + + def get_exconly(self, excinfo, indent=4, markall=False): + lines = [] + indent = " " * indent + # get the real exception information out + exlines = excinfo.exconly(tryshort=True).split("\n") + failindent = self.fail_marker + indent[1:] + for line in exlines: + lines.append(failindent + line) + if not markall: + failindent = indent + return lines + + def repr_locals(self, 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("%-10s = %s" % (name, str_repr)) - # else: - # self._line("%-10s =\\" % (name,)) - # # XXX - # pprint.pprint(value, stream=self.excinfowriter) - return ReprLocals(lines) - - def repr_traceback_entry(self, entry, excinfo=None): - import _pytest._code - - source = self._getentrysource(entry) - if source is None: - source = _pytest._code.Source("???") - line_index = 0 - else: - # entry.getfirstlinesource() can be -1, should be 0 on jython - line_index = entry.lineno - max(entry.getfirstlinesource(), 0) - - lines = [] - style = entry._repr_style - if style is None: - style = self.style - if style in ("short", "long"): - 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) - filelocrepr = ReprFileLocation(path, entry.lineno + 1, message) - localsrepr = None - if not short: - localsrepr = self.repr_locals(entry.locals) - return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style) - 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 repr_traceback(self, excinfo): - traceback = excinfo.traceback - if self.tbfilter: - traceback = traceback.filter() - - if is_recursion_error(excinfo): - traceback, extraline = self._truncate_recursive_traceback(traceback) - else: - extraline = None - - last = traceback[-1] - entries = [] - 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): - """ - 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 - extraline = ( - "!!! 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=safe_str(e), - max_frames=max_frames, - total=len(traceback), - ) - traceback = traceback[:max_frames] + traceback[-max_frames:] - 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): - if _PY2: - reprtraceback = self.repr_traceback(excinfo) - reprcrash = excinfo._getreprcrash() - - return ReprExceptionInfo(reprtraceback, reprcrash) - else: - repr_chain = [] - e = excinfo.value - descr = None - seen = set() - while e is not None and id(e) not in seen: - seen.add(id(e)) - if excinfo: - reprtraceback = self.repr_traceback(excinfo) - reprcrash = excinfo._getreprcrash() - else: - # fallback to native repr if the exception doesn't have a traceback: - # ExceptionInfo objects require a full traceback to work - reprtraceback = ReprTracebackNative( - 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__ - excinfo = ( - ExceptionInfo((type(e), e, e.__traceback__)) - if e.__traceback__ - else None - ) - descr = "The above exception was the direct cause of the following exception:" - elif ( - e.__context__ is not None - and not e.__suppress_context__ - and self.chain - ): - e = e.__context__ - excinfo = ( - ExceptionInfo((type(e), e, e.__traceback__)) - if e.__traceback__ - else None - ) - descr = "During handling of the above exception, another exception occurred:" - else: - e = None - repr_chain.reverse() - return ExceptionChainRepr(repr_chain) - - -class TerminalRepr(object): - def __str__(self): - s = self.__unicode__() - if _PY2: - s = s.encode("utf-8") - return s - - def __unicode__(self): - # FYI this is called from pytest-xdist's serialization of exception - # information. - io = py.io.TextIO() - tw = py.io.TerminalWriter(file=io) - self.toterminal(tw) - return io.getvalue().strip() - - def __repr__(self): - return "<%s instance at %0x>" % (self.__class__, id(self)) - - -class ExceptionRepr(TerminalRepr): - def __init__(self): - self.sections = [] - - def addsection(self, name, content, sep="-"): - self.sections.append((name, content, sep)) - - def toterminal(self, tw): - for name, content, sep in self.sections: - tw.sep(sep, name) - tw.line(content) - - -class ExceptionChainRepr(ExceptionRepr): - def __init__(self, chain): - super(ExceptionChainRepr, self).__init__() - self.chain = chain - # reprcrash and reprtraceback of the outermost (the newest) exception - # in the chain - self.reprtraceback = chain[-1][0] - self.reprcrash = chain[-1][1] - - def toterminal(self, tw): - for element in self.chain: - element[0].toterminal(tw) - if element[2] is not None: - tw.line("") - tw.line(element[2], yellow=True) - super(ExceptionChainRepr, self).toterminal(tw) - - -class ReprExceptionInfo(ExceptionRepr): - def __init__(self, reprtraceback, reprcrash): - super(ReprExceptionInfo, self).__init__() - self.reprtraceback = reprtraceback - self.reprcrash = reprcrash - - def toterminal(self, tw): - self.reprtraceback.toterminal(tw) - super(ReprExceptionInfo, self).toterminal(tw) - - -class ReprTraceback(TerminalRepr): - entrysep = "_ " - - def __init__(self, reprentries, extraline, style): - self.reprentries = reprentries - self.extraline = extraline - self.style = style - - def toterminal(self, tw): - # 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): - def __init__(self, tblines): - self.style = "native" - self.reprentries = [ReprEntryNative(tblines)] - self.extraline = None - - -class ReprEntryNative(TerminalRepr): - style = "native" - - def __init__(self, tblines): - self.lines = tblines - - def toterminal(self, tw): - tw.write("".join(self.lines)) - - -class ReprEntry(TerminalRepr): - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): - self.lines = lines - self.reprfuncargs = reprfuncargs - self.reprlocals = reprlocals - self.reprfileloc = filelocrepr - self.style = style - - def toterminal(self, tw): - if self.style == "short": - self.reprfileloc.toterminal(tw) - for line in self.lines: - red = line.startswith("E ") - tw.line(line, bold=True, red=red) - # tw.line("") - return - if self.reprfuncargs: - self.reprfuncargs.toterminal(tw) - for line in self.lines: - red = line.startswith("E ") - tw.line(line, bold=True, red=red) - if self.reprlocals: - tw.line("") - self.reprlocals.toterminal(tw) - if self.reprfileloc: - if self.lines: - tw.line("") - self.reprfileloc.toterminal(tw) - - def __str__(self): - return "%s\n%s\n%s" % ("\n".join(self.lines), self.reprlocals, self.reprfileloc) - - -class ReprFileLocation(TerminalRepr): - def __init__(self, path, lineno, message): - self.path = str(path) - self.lineno = lineno - self.message = message - - def toterminal(self, tw): - # filename and lineno output for each entry, - # using an output format that most editors unterstand - msg = self.message - i = msg.find("\n") - if i != -1: - msg = msg[:i] - tw.write(self.path, bold=True, red=True) - tw.line(":%s: %s" % (self.lineno, msg)) - - -class ReprLocals(TerminalRepr): - def __init__(self, lines): - self.lines = lines - - def toterminal(self, tw): - for line in self.lines: - tw.line(line) - - -class ReprFuncArgs(TerminalRepr): - def __init__(self, args): - self.args = args - - def toterminal(self, tw): - if self.args: - linesofar = "" - for name, value in self.args: - ns = "%s = %s" % (safe_str(name), safe_str(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("") - - -def getrawcode(obj, trycall=True): - """ return code object for given function. """ - try: - return obj.__code__ - except AttributeError: - obj = getattr(obj, "im_func", obj) - obj = getattr(obj, "func_code", obj) - obj = getattr(obj, "f_code", obj) - obj = getattr(obj, "__code__", obj) - if trycall and not hasattr(obj, "co_firstlineno"): - if hasattr(obj, "__call__") and not inspect.isclass(obj): - x = getrawcode(obj.__call__, trycall=False) - if hasattr(x, "co_firstlineno"): - return x - return obj - - -if PY35: # RecursionError introduced in 3.5 - - def is_recursion_error(excinfo): - return excinfo.errisinstance(RecursionError) # noqa - - -else: - - def is_recursion_error(excinfo): - if not excinfo.errisinstance(RuntimeError): - return False - try: - return "maximum recursion depth exceeded" in str(excinfo.value) - except UnicodeError: - return False - - -# 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 -# for better maintenance - -_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) -# pluggy is either a package or a single module depending on the version -if _PLUGGY_DIR.basename == "__init__.py": - _PLUGGY_DIR = _PLUGGY_DIR.dirpath() -_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() -_PY_DIR = py.path.local(py.__file__).dirpath() - - -def filter_traceback(entry): - """Return True if a TracebackEntry instance should be removed from tracebacks: - * 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 - # entry.path might point to a non-existing file, in which case it will - # also return a str object. see #1133 - p = py.path.local(entry.path) - return ( - not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) - ) + # if len(str_repr) < 70 or not isinstance(value, + # (list, tuple, dict)): + lines.append("%-10s = %s" % (name, str_repr)) + # else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) + + def repr_traceback_entry(self, entry, excinfo=None): + import _pytest._code + + source = self._getentrysource(entry) + if source is None: + source = _pytest._code.Source("???") + line_index = 0 + else: + # entry.getfirstlinesource() can be -1, should be 0 on jython + line_index = entry.lineno - max(entry.getfirstlinesource(), 0) + + lines = [] + style = entry._repr_style + if style is None: + style = self.style + if style in ("short", "long"): + 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) + filelocrepr = ReprFileLocation(path, entry.lineno + 1, message) + localsrepr = None + if not short: + localsrepr = self.repr_locals(entry.locals) + return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style) + 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 repr_traceback(self, excinfo): + traceback = excinfo.traceback + if self.tbfilter: + traceback = traceback.filter() + + if is_recursion_error(excinfo): + traceback, extraline = self._truncate_recursive_traceback(traceback) + else: + extraline = None + + last = traceback[-1] + entries = [] + 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): + """ + 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 + extraline = ( + "!!! 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=safe_str(e), + max_frames=max_frames, + total=len(traceback), + ) + traceback = traceback[:max_frames] + traceback[-max_frames:] + 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): + if _PY2: + reprtraceback = self.repr_traceback(excinfo) + reprcrash = excinfo._getreprcrash() + + return ReprExceptionInfo(reprtraceback, reprcrash) + else: + repr_chain = [] + e = excinfo.value + descr = None + seen = set() + while e is not None and id(e) not in seen: + seen.add(id(e)) + if excinfo: + reprtraceback = self.repr_traceback(excinfo) + reprcrash = excinfo._getreprcrash() + else: + # fallback to native repr if the exception doesn't have a traceback: + # ExceptionInfo objects require a full traceback to work + reprtraceback = ReprTracebackNative( + 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__ + excinfo = ( + ExceptionInfo((type(e), e, e.__traceback__)) + if e.__traceback__ + else None + ) + descr = "The above exception was the direct cause of the following exception:" + elif ( + e.__context__ is not None + and not e.__suppress_context__ + and self.chain + ): + e = e.__context__ + excinfo = ( + ExceptionInfo((type(e), e, e.__traceback__)) + if e.__traceback__ + else None + ) + descr = "During handling of the above exception, another exception occurred:" + else: + e = None + repr_chain.reverse() + return ExceptionChainRepr(repr_chain) + + +class TerminalRepr(object): + def __str__(self): + s = self.__unicode__() + if _PY2: + s = s.encode("utf-8") + return s + + def __unicode__(self): + # FYI this is called from pytest-xdist's serialization of exception + # information. + io = py.io.TextIO() + tw = py.io.TerminalWriter(file=io) + self.toterminal(tw) + return io.getvalue().strip() + + def __repr__(self): + return "<%s instance at %0x>" % (self.__class__, id(self)) + + +class ExceptionRepr(TerminalRepr): + def __init__(self): + self.sections = [] + + def addsection(self, name, content, sep="-"): + self.sections.append((name, content, sep)) + + def toterminal(self, tw): + for name, content, sep in self.sections: + tw.sep(sep, name) + tw.line(content) + + +class ExceptionChainRepr(ExceptionRepr): + def __init__(self, chain): + super(ExceptionChainRepr, self).__init__() + self.chain = chain + # reprcrash and reprtraceback of the outermost (the newest) exception + # in the chain + self.reprtraceback = chain[-1][0] + self.reprcrash = chain[-1][1] + + def toterminal(self, tw): + for element in self.chain: + element[0].toterminal(tw) + if element[2] is not None: + tw.line("") + tw.line(element[2], yellow=True) + super(ExceptionChainRepr, self).toterminal(tw) + + +class ReprExceptionInfo(ExceptionRepr): + def __init__(self, reprtraceback, reprcrash): + super(ReprExceptionInfo, self).__init__() + self.reprtraceback = reprtraceback + self.reprcrash = reprcrash + + def toterminal(self, tw): + self.reprtraceback.toterminal(tw) + super(ReprExceptionInfo, self).toterminal(tw) + + +class ReprTraceback(TerminalRepr): + entrysep = "_ " + + def __init__(self, reprentries, extraline, style): + self.reprentries = reprentries + self.extraline = extraline + self.style = style + + def toterminal(self, tw): + # 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): + def __init__(self, tblines): + self.style = "native" + self.reprentries = [ReprEntryNative(tblines)] + self.extraline = None + + +class ReprEntryNative(TerminalRepr): + style = "native" + + def __init__(self, tblines): + self.lines = tblines + + def toterminal(self, tw): + tw.write("".join(self.lines)) + + +class ReprEntry(TerminalRepr): + def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): + self.lines = lines + self.reprfuncargs = reprfuncargs + self.reprlocals = reprlocals + self.reprfileloc = filelocrepr + self.style = style + + def toterminal(self, tw): + if self.style == "short": + self.reprfileloc.toterminal(tw) + for line in self.lines: + red = line.startswith("E ") + tw.line(line, bold=True, red=red) + # tw.line("") + return + if self.reprfuncargs: + self.reprfuncargs.toterminal(tw) + for line in self.lines: + red = line.startswith("E ") + tw.line(line, bold=True, red=red) + if self.reprlocals: + tw.line("") + self.reprlocals.toterminal(tw) + if self.reprfileloc: + if self.lines: + tw.line("") + self.reprfileloc.toterminal(tw) + + def __str__(self): + return "%s\n%s\n%s" % ("\n".join(self.lines), self.reprlocals, self.reprfileloc) + + +class ReprFileLocation(TerminalRepr): + def __init__(self, path, lineno, message): + self.path = str(path) + self.lineno = lineno + self.message = message + + def toterminal(self, tw): + # filename and lineno output for each entry, + # using an output format that most editors unterstand + msg = self.message + i = msg.find("\n") + if i != -1: + msg = msg[:i] + tw.write(self.path, bold=True, red=True) + tw.line(":%s: %s" % (self.lineno, msg)) + + +class ReprLocals(TerminalRepr): + def __init__(self, lines): + self.lines = lines + + def toterminal(self, tw): + for line in self.lines: + tw.line(line) + + +class ReprFuncArgs(TerminalRepr): + def __init__(self, args): + self.args = args + + def toterminal(self, tw): + if self.args: + linesofar = "" + for name, value in self.args: + ns = "%s = %s" % (safe_str(name), safe_str(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("") + + +def getrawcode(obj, trycall=True): + """ return code object for given function. """ + try: + return obj.__code__ + except AttributeError: + obj = getattr(obj, "im_func", obj) + obj = getattr(obj, "func_code", obj) + obj = getattr(obj, "f_code", obj) + obj = getattr(obj, "__code__", obj) + if trycall and not hasattr(obj, "co_firstlineno"): + if hasattr(obj, "__call__") and not inspect.isclass(obj): + x = getrawcode(obj.__call__, trycall=False) + if hasattr(x, "co_firstlineno"): + return x + return obj + + +if PY35: # RecursionError introduced in 3.5 + + def is_recursion_error(excinfo): + return excinfo.errisinstance(RecursionError) # noqa + + +else: + + def is_recursion_error(excinfo): + if not excinfo.errisinstance(RuntimeError): + return False + try: + return "maximum recursion depth exceeded" in str(excinfo.value) + except UnicodeError: + return False + + +# 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 +# for better maintenance + +_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) +# pluggy is either a package or a single module depending on the version +if _PLUGGY_DIR.basename == "__init__.py": + _PLUGGY_DIR = _PLUGGY_DIR.dirpath() +_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() +_PY_DIR = py.path.local(py.__file__).dirpath() + + +def filter_traceback(entry): + """Return True if a TracebackEntry instance should be removed from tracebacks: + * 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 + # entry.path might point to a non-existing file, in which case it will + # also return a str object. see #1133 + p = py.path.local(entry.path) + return ( + not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) + ) diff --git a/contrib/python/pytest/py2/_pytest/_code/source.py b/contrib/python/pytest/py2/_pytest/_code/source.py index c329c7732a..b35e97b9ce 100644 --- a/contrib/python/pytest/py2/_pytest/_code/source.py +++ b/contrib/python/pytest/py2/_pytest/_code/source.py @@ -1,324 +1,324 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import ast -import inspect -import linecache -import sys -import textwrap -import tokenize -import warnings -from ast import PyCF_ONLY_AST as _AST_FLAG -from bisect import bisect_right - -import py -import six - - -class Source(object): - """ an immutable object holding a source code fragment, - possibly deindenting it. - """ - - _compilecounter = 0 - - def __init__(self, *parts, **kwargs): - self.lines = lines = [] - de = kwargs.get("deindent", True) - for part in parts: - if not part: - partlines = [] - elif isinstance(part, Source): - partlines = part.lines - elif isinstance(part, (tuple, list)): - partlines = [x.rstrip("\n") for x in part] - elif isinstance(part, six.string_types): - partlines = part.split("\n") - else: - partlines = getsource(part, deindent=de).lines - if de: - partlines = deindent(partlines) - lines.extend(partlines) - - def __eq__(self, other): - try: - return self.lines == other.lines - except AttributeError: - if isinstance(other, str): - return str(self) == other - return False - - __hash__ = None - - def __getitem__(self, key): - 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 __len__(self): - return len(self.lines) - - def strip(self): - """ 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 - - def putaround(self, before="", after="", indent=" " * 4): - """ return a copy of the source object with - 'before' and 'after' wrapped around it. - """ - before = Source(before) - after = Source(after) - newsource = Source() - lines = [(indent + line) for line in self.lines] - newsource.lines = before.lines + lines + after.lines - return newsource - - def indent(self, indent=" " * 4): - """ 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 - - def getstatement(self, lineno): - """ return Source statement which contains the - given linenumber (counted from 0). - """ - start, end = self.getstatementrange(lineno) - return self[start:end] - - def getstatementrange(self, lineno): - """ 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 - - def deindent(self): - """return a new source object deindented.""" - newsource = Source() - newsource.lines[:] = deindent(self.lines) - return newsource - - def isparseable(self, deindent=True): - """ return True if source is parseable, heuristically - deindenting it by default. - """ - if deindent: - source = str(self.deindent()) - else: - source = str(self) - try: +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import ast +import inspect +import linecache +import sys +import textwrap +import tokenize +import warnings +from ast import PyCF_ONLY_AST as _AST_FLAG +from bisect import bisect_right + +import py +import six + + +class Source(object): + """ an immutable object holding a source code fragment, + possibly deindenting it. + """ + + _compilecounter = 0 + + def __init__(self, *parts, **kwargs): + self.lines = lines = [] + de = kwargs.get("deindent", True) + for part in parts: + if not part: + partlines = [] + elif isinstance(part, Source): + partlines = part.lines + elif isinstance(part, (tuple, list)): + partlines = [x.rstrip("\n") for x in part] + elif isinstance(part, six.string_types): + partlines = part.split("\n") + else: + partlines = getsource(part, deindent=de).lines + if de: + partlines = deindent(partlines) + lines.extend(partlines) + + def __eq__(self, other): + try: + return self.lines == other.lines + except AttributeError: + if isinstance(other, str): + return str(self) == other + return False + + __hash__ = None + + def __getitem__(self, key): + 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 __len__(self): + return len(self.lines) + + def strip(self): + """ 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 + + def putaround(self, before="", after="", indent=" " * 4): + """ return a copy of the source object with + 'before' and 'after' wrapped around it. + """ + before = Source(before) + after = Source(after) + newsource = Source() + lines = [(indent + line) for line in self.lines] + newsource.lines = before.lines + lines + after.lines + return newsource + + def indent(self, indent=" " * 4): + """ 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 + + def getstatement(self, lineno): + """ return Source statement which contains the + given linenumber (counted from 0). + """ + start, end = self.getstatementrange(lineno) + return self[start:end] + + def getstatementrange(self, lineno): + """ 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 + + def deindent(self): + """return a new source object deindented.""" + newsource = Source() + newsource.lines[:] = deindent(self.lines) + return newsource + + def isparseable(self, deindent=True): + """ return True if source is parseable, heuristically + deindenting it by default. + """ + if deindent: + source = str(self.deindent()) + else: + source = str(self) + try: ast.parse(source) except (SyntaxError, ValueError, TypeError): - return False - else: - return True - - def __str__(self): - return "\n".join(self.lines) - - def compile( - self, filename=None, mode="exec", flag=0, dont_inherit=0, _genframe=None - ): - """ return compiled code object. if filename is None - invent an artificial filename which displays - the source/line position of the caller frame. - """ - if not filename or py.path.local(filename).check(file=0): - if _genframe is None: - _genframe = sys._getframe(1) # the caller - fn, lineno = _genframe.f_code.co_filename, _genframe.f_lineno - base = "<%d-codegen " % self._compilecounter - self.__class__._compilecounter += 1 - if not filename: - filename = base + "%s:%d>" % (fn, lineno) - else: - filename = base + "%r %s:%d>" % (filename, fn, lineno) - source = "\n".join(self.lines) + "\n" - try: - co = compile(source, filename, mode, flag) - except SyntaxError: - ex = sys.exc_info()[1] - # re-represent syntax errors from parsing python strings - msglines = self.lines[: ex.lineno] - if ex.offset: - msglines.append(" " * ex.offset + "^") - msglines.append("(code was compiled probably from here: %s)" % filename) - newex = SyntaxError("\n".join(msglines)) - newex.offset = ex.offset - newex.lineno = ex.lineno - newex.text = ex.text - raise newex - else: - if flag & _AST_FLAG: - return co - lines = [(x + "\n") for x in self.lines] - linecache.cache[filename] = (1, None, lines, filename) - return co - - -# -# public API shortcut functions -# - - -def compile_(source, filename=None, mode="exec", flags=0, dont_inherit=0): - """ compile the given source to a raw code object, - and maintain an internal cache which allows later - retrieval of the source code for the code object - and any recursively created code objects. - """ - if isinstance(source, ast.AST): - # XXX should Source support having AST? - return compile(source, filename, mode, flags, dont_inherit) - _genframe = sys._getframe(1) # the caller - s = Source(source) - co = s.compile(filename, mode, flags, _genframe=_genframe) - return co - - -def getfslineno(obj): - """ Return source location (path, lineno) for the given object. + return False + else: + return True + + def __str__(self): + return "\n".join(self.lines) + + def compile( + self, filename=None, mode="exec", flag=0, dont_inherit=0, _genframe=None + ): + """ return compiled code object. if filename is None + invent an artificial filename which displays + the source/line position of the caller frame. + """ + if not filename or py.path.local(filename).check(file=0): + if _genframe is None: + _genframe = sys._getframe(1) # the caller + fn, lineno = _genframe.f_code.co_filename, _genframe.f_lineno + base = "<%d-codegen " % self._compilecounter + self.__class__._compilecounter += 1 + if not filename: + filename = base + "%s:%d>" % (fn, lineno) + else: + filename = base + "%r %s:%d>" % (filename, fn, lineno) + source = "\n".join(self.lines) + "\n" + try: + co = compile(source, filename, mode, flag) + except SyntaxError: + ex = sys.exc_info()[1] + # re-represent syntax errors from parsing python strings + msglines = self.lines[: ex.lineno] + if ex.offset: + msglines.append(" " * ex.offset + "^") + msglines.append("(code was compiled probably from here: %s)" % filename) + newex = SyntaxError("\n".join(msglines)) + newex.offset = ex.offset + newex.lineno = ex.lineno + newex.text = ex.text + raise newex + else: + if flag & _AST_FLAG: + return co + lines = [(x + "\n") for x in self.lines] + linecache.cache[filename] = (1, None, lines, filename) + return co + + +# +# public API shortcut functions +# + + +def compile_(source, filename=None, mode="exec", flags=0, dont_inherit=0): + """ compile the given source to a raw code object, + and maintain an internal cache which allows later + retrieval of the source code for the code object + and any recursively created code objects. + """ + if isinstance(source, ast.AST): + # XXX should Source support having AST? + return compile(source, filename, mode, flags, dont_inherit) + _genframe = sys._getframe(1) # the caller + s = Source(source) + co = s.compile(filename, mode, flags, _genframe=_genframe) + return co + + +def getfslineno(obj): + """ Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). The line number is 0-based. - """ - from .code import Code - - try: - code = Code(obj) - except TypeError: - try: - fn = inspect.getsourcefile(obj) or inspect.getfile(obj) - except TypeError: - return "", -1 - - fspath = fn and py.path.local(fn) or None - lineno = -1 - if fspath: - try: - _, lineno = findsource(obj) - except IOError: - pass - else: - fspath = code.path - lineno = code.firstlineno - assert isinstance(lineno, int) - return fspath, lineno - - -# -# helper functions -# - - -def findsource(obj): - try: - sourcelines, lineno = inspect.findsource(obj) + """ + from .code import Code + + try: + code = Code(obj) + except TypeError: + try: + fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + except TypeError: + return "", -1 + + fspath = fn and py.path.local(fn) or None + lineno = -1 + if fspath: + try: + _, lineno = findsource(obj) + except IOError: + pass + else: + fspath = code.path + lineno = code.firstlineno + assert isinstance(lineno, int) + return fspath, lineno + + +# +# helper functions +# + + +def 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 - - -def getsource(obj, **kwargs): - from .code import getrawcode - - obj = getrawcode(obj) - try: - strsrc = inspect.getsource(obj) - except IndentationError: - strsrc = '"Buggy python version consider upgrading, cannot get source"' - assert isinstance(strsrc, str) - return Source(strsrc, **kwargs) - - -def deindent(lines): - return textwrap.dedent("\n".join(lines)).splitlines() - - -def get_statement_startend2(lineno, node): - import ast - - # flatten all statements and except handlers into one lineno-list - # AST's line numbers start indexing at 1 - values = [] - for x in ast.walk(node): - if isinstance(x, (ast.stmt, ast.ExceptHandler)): - values.append(x.lineno - 1) - for name in ("finalbody", "orelse"): - val = getattr(x, name, None) - 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 - - -def getstatementrange_ast(lineno, source, assertion=False, astnode=None): - 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") - astnode = compile(content, "source", "exec", _AST_FLAG) - - 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: - # 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() - # 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 - - # 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 + return None, -1 + source = Source() + source.lines = [line.rstrip() for line in sourcelines] + return source, lineno + + +def getsource(obj, **kwargs): + from .code import getrawcode + + obj = getrawcode(obj) + try: + strsrc = inspect.getsource(obj) + except IndentationError: + strsrc = '"Buggy python version consider upgrading, cannot get source"' + assert isinstance(strsrc, str) + return Source(strsrc, **kwargs) + + +def deindent(lines): + return textwrap.dedent("\n".join(lines)).splitlines() + + +def get_statement_startend2(lineno, node): + import ast + + # flatten all statements and except handlers into one lineno-list + # AST's line numbers start indexing at 1 + values = [] + for x in ast.walk(node): + if isinstance(x, (ast.stmt, ast.ExceptHandler)): + values.append(x.lineno - 1) + for name in ("finalbody", "orelse"): + val = getattr(x, name, None) + 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 + + +def getstatementrange_ast(lineno, source, assertion=False, astnode=None): + 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") + astnode = compile(content, "source", "exec", _AST_FLAG) + + 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: + # 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() + # 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 + + # 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 diff --git a/contrib/python/pytest/py2/_pytest/_version.py b/contrib/python/pytest/py2/_pytest/_version.py index c9d35307e4..3d587586fc 100644 --- a/contrib/python/pytest/py2/_pytest/_version.py +++ b/contrib/python/pytest/py2/_pytest/_version.py @@ -1,4 +1,4 @@ -# 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 = '4.6.11' diff --git a/contrib/python/pytest/py2/_pytest/assertion/__init__.py b/contrib/python/pytest/py2/_pytest/assertion/__init__.py index f5e0972876..6b6abb863a 100644 --- a/contrib/python/pytest/py2/_pytest/assertion/__init__.py +++ b/contrib/python/pytest/py2/_pytest/assertion/__init__.py @@ -1,156 +1,156 @@ # -*- coding: utf-8 -*- -""" -support for presenting detailed information in failing assertions. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import sys - -import six - -from _pytest.assertion import rewrite -from _pytest.assertion import truncate -from _pytest.assertion import util - - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group.addoption( - "--assert", - action="store", - dest="assertmode", - choices=("rewrite", "plain"), - default="rewrite", - metavar="MODE", - help="""Control assertion debugging tools. 'plain' - performs no assertion debugging. 'rewrite' - (the default) rewrites assert statements in - test modules on import to provide assert - expression information.""", - ) - - -def register_assert_rewrite(*names): - """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. - - :raise TypeError: if the given module names are not strings. - """ - for name in names: - if not isinstance(name, str): - msg = "expected module names as *args, got {0} instead" - raise TypeError(msg.format(repr(names))) - for hook in sys.meta_path: - if isinstance(hook, rewrite.AssertionRewritingHook): - importhook = hook - break - else: - importhook = DummyRewriteHook() - importhook.mark_rewrite(*names) - - -class DummyRewriteHook(object): - """A no-op import hook for when rewriting is disabled.""" - - def mark_rewrite(self, *names): - pass - - -class AssertionState(object): - """State for the assertion plugin.""" - - def __init__(self, config, mode): - self.mode = mode - self.trace = config.trace.root.get("assertion") - self.hook = None - - -def install_importhook(config): - """Try to install the rewrite hook, raise SystemError if it fails.""" - # Jython has an AST bug that make the assertion rewriting hook malfunction. - if sys.platform.startswith("java"): - raise SystemError("rewrite not supported") - - config._assertstate = AssertionState(config, "rewrite") - config._assertstate.hook = hook = rewrite.AssertionRewritingHook(config) - sys.meta_path.insert(0, hook) - config._assertstate.trace("installed rewrite import hook") - - def undo(): - hook = config._assertstate.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): - # this hook is only called when test modules are collected - # so for example not in the master process of pytest-xdist - # (which does not collect test modules) - assertstate = getattr(session.config, "_assertstate", None) - if assertstate: - if assertstate.hook is not None: - assertstate.hook.set_session(session) - - -def pytest_runtest_setup(item): - """Setup the pytest_assertrepr_compare hook - - The newinterpret and rewrite modules 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. - """ - - def callbinrepr(op, left, right): - """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. - """ - hook_result = item.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] - res = six.text_type("\n~").join(new_expl) - if item.config.getvalue("assertmode") == "rewrite": - res = res.replace("%", "%%") - return res - - util._reprcompare = callbinrepr - - -def pytest_runtest_teardown(item): - util._reprcompare = None - - -def pytest_sessionfinish(session): - assertstate = getattr(session.config, "_assertstate", None) - if assertstate: - if assertstate.hook is not None: - assertstate.hook.set_session(None) - - -# Expose this plugin's implementation for the pytest_assertrepr_compare hook -pytest_assertrepr_compare = util.assertrepr_compare +""" +support for presenting detailed information in failing assertions. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys + +import six + +from _pytest.assertion import rewrite +from _pytest.assertion import truncate +from _pytest.assertion import util + + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group.addoption( + "--assert", + action="store", + dest="assertmode", + choices=("rewrite", "plain"), + default="rewrite", + metavar="MODE", + help="""Control assertion debugging tools. 'plain' + performs no assertion debugging. 'rewrite' + (the default) rewrites assert statements in + test modules on import to provide assert + expression information.""", + ) + + +def register_assert_rewrite(*names): + """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. + + :raise TypeError: if the given module names are not strings. + """ + for name in names: + if not isinstance(name, str): + msg = "expected module names as *args, got {0} instead" + raise TypeError(msg.format(repr(names))) + for hook in sys.meta_path: + if isinstance(hook, rewrite.AssertionRewritingHook): + importhook = hook + break + else: + importhook = DummyRewriteHook() + importhook.mark_rewrite(*names) + + +class DummyRewriteHook(object): + """A no-op import hook for when rewriting is disabled.""" + + def mark_rewrite(self, *names): + pass + + +class AssertionState(object): + """State for the assertion plugin.""" + + def __init__(self, config, mode): + self.mode = mode + self.trace = config.trace.root.get("assertion") + self.hook = None + + +def install_importhook(config): + """Try to install the rewrite hook, raise SystemError if it fails.""" + # Jython has an AST bug that make the assertion rewriting hook malfunction. + if sys.platform.startswith("java"): + raise SystemError("rewrite not supported") + + config._assertstate = AssertionState(config, "rewrite") + config._assertstate.hook = hook = rewrite.AssertionRewritingHook(config) + sys.meta_path.insert(0, hook) + config._assertstate.trace("installed rewrite import hook") + + def undo(): + hook = config._assertstate.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): + # this hook is only called when test modules are collected + # so for example not in the master process of pytest-xdist + # (which does not collect test modules) + assertstate = getattr(session.config, "_assertstate", None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(session) + + +def pytest_runtest_setup(item): + """Setup the pytest_assertrepr_compare hook + + The newinterpret and rewrite modules 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. + """ + + def callbinrepr(op, left, right): + """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. + """ + hook_result = item.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] + res = six.text_type("\n~").join(new_expl) + if item.config.getvalue("assertmode") == "rewrite": + res = res.replace("%", "%%") + return res + + util._reprcompare = callbinrepr + + +def pytest_runtest_teardown(item): + util._reprcompare = None + + +def pytest_sessionfinish(session): + assertstate = getattr(session.config, "_assertstate", None) + if assertstate: + if assertstate.hook is not None: + assertstate.hook.set_session(None) + + +# Expose this plugin's implementation for the pytest_assertrepr_compare hook +pytest_assertrepr_compare = util.assertrepr_compare diff --git a/contrib/python/pytest/py2/_pytest/assertion/rewrite.py b/contrib/python/pytest/py2/_pytest/assertion/rewrite.py index 6f619ed6a1..6cfd81a32f 100644 --- a/contrib/python/pytest/py2/_pytest/assertion/rewrite.py +++ b/contrib/python/pytest/py2/_pytest/assertion/rewrite.py @@ -1,834 +1,834 @@ # -*- coding: utf-8 -*- -"""Rewrite assertion AST to produce nice error messages""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - +"""Rewrite assertion AST to produce nice error messages""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + import warnings warnings.filterwarnings("ignore", category=DeprecationWarning, module="_pytest.assertion.rewrite") -import ast -import errno -import imp -import itertools -import marshal -import os -import re -import string -import struct -import sys -import types - -import atomicwrites -import py -import six - +import ast +import errno +import imp +import itertools +import marshal +import os +import re +import string +import struct +import sys +import types + +import atomicwrites +import py +import six + from _pytest._io.saferepr import saferepr -from _pytest.assertion import util +from _pytest.assertion import util from _pytest.assertion.util import ( # noqa: F401 format_explanation as _format_explanation, ) -from _pytest.compat import spec_from_file_location -from _pytest.pathlib import fnmatch_ex -from _pytest.pathlib import PurePath - -# pytest caches rewritten pycs in __pycache__. -if hasattr(imp, "get_tag"): - PYTEST_TAG = imp.get_tag() + "-PYTEST" -else: - if hasattr(sys, "pypy_version_info"): - impl = "pypy" - elif sys.platform == "java": - impl = "jython" - else: - impl = "cpython" - ver = sys.version_info - PYTEST_TAG = "%s-%s%s-PYTEST" % (impl, ver[0], ver[1]) - del ver, impl - -PYC_EXT = ".py" + (__debug__ and "c" or "o") -PYC_TAIL = "." + PYTEST_TAG + PYC_EXT - -ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3 - -if sys.version_info >= (3, 5): - ast_Call = ast.Call -else: - - def ast_Call(a, b, c): - return ast.Call(a, b, c, None, None) - - -class AssertionRewritingHook(object): - """PEP302 Import hook which rewrites asserts.""" - - def __init__(self, config): - self.config = config +from _pytest.compat import spec_from_file_location +from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import PurePath + +# pytest caches rewritten pycs in __pycache__. +if hasattr(imp, "get_tag"): + PYTEST_TAG = imp.get_tag() + "-PYTEST" +else: + if hasattr(sys, "pypy_version_info"): + impl = "pypy" + elif sys.platform == "java": + impl = "jython" + else: + impl = "cpython" + ver = sys.version_info + PYTEST_TAG = "%s-%s%s-PYTEST" % (impl, ver[0], ver[1]) + del ver, impl + +PYC_EXT = ".py" + (__debug__ and "c" or "o") +PYC_TAIL = "." + PYTEST_TAG + PYC_EXT + +ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3 + +if sys.version_info >= (3, 5): + ast_Call = ast.Call +else: + + def ast_Call(a, b, c): + return ast.Call(a, b, c, None, None) + + +class AssertionRewritingHook(object): + """PEP302 Import hook which rewrites asserts.""" + + def __init__(self, config): + self.config = config try: self.fnpats = config.getini("python_files") except ValueError: self.fnpats = ["test_*.py", "*_test.py"] - self.session = None - self.modules = {} - self._rewritten_names = set() - self._must_rewrite = 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"} - self._marked_for_rewrite_cache = {} - self._session_paths_checked = False - - def set_session(self, session): - self.session = session - self._session_paths_checked = False - - def _imp_find_module(self, name, path=None): - """Indirection so we can mock calls to find_module originated from the hook during testing""" - return imp.find_module(name, path) - - def find_module(self, name, path=None): - if self._writing_pyc: - return None - state = self.config._assertstate - if self._early_rewrite_bailout(name, state): - return None - state.trace("find_module called for: %s" % name) - names = name.rsplit(".", 1) - lastname = names[-1] - pth = None - if path is not None: - # Starting with Python 3.3, path is a _NamespacePath(), which - # causes problems if not converted to list. - path = list(path) - if len(path) == 1: - pth = path[0] - if pth is None: - try: - fd, fn, desc = self._imp_find_module(lastname, path) - except ImportError: - return None - if fd is not None: - fd.close() - tp = desc[2] - if tp == imp.PY_COMPILED: - if hasattr(imp, "source_from_cache"): - try: - fn = imp.source_from_cache(fn) - except ValueError: - # Python 3 doesn't like orphaned but still-importable - # .pyc files. - fn = fn[:-1] - else: - fn = fn[:-1] - elif tp != imp.PY_SOURCE: - # Don't know what this is. - return None - else: - fn = os.path.join(pth, name.rpartition(".")[2] + ".py") - - fn_pypath = py.path.local(fn) - if not self._should_rewrite(name, fn_pypath, state): - return None - - self._rewritten_names.add(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 - cache_dir = os.path.join(fn_pypath.dirname, "__pycache__") - if write: - try: - os.mkdir(cache_dir) - except OSError: - e = sys.exc_info()[1].errno - if e == errno.EEXIST: - # Either the __pycache__ directory already exists (the - # common case) or it's blocked by a non-dir node. In the - # latter case, we'll ignore it in _write_pyc. - pass - elif e in [errno.ENOENT, errno.ENOTDIR]: - # One of the path components was not a directory, likely - # because we're in a zip file. - write = False - elif e in [errno.EACCES, errno.EROFS, errno.EPERM]: - state.trace("read only directory: %r" % fn_pypath.dirname) - write = False - else: - raise - cache_name = fn_pypath.basename[:-3] + PYC_TAIL - pyc = os.path.join(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... - co = _read_pyc(fn_pypath, pyc, state.trace) - if co is None: - state.trace("rewriting %r" % (fn,)) - source_stat, co = _rewrite_test(self.config, fn_pypath) - if co is None: - # Probably a SyntaxError in the test. - return None - if write: - self._writing_pyc = True - try: - _write_pyc(state, co, source_stat, pyc) - finally: - self._writing_pyc = False - else: - state.trace("found cached rewritten pyc for %r" % (fn,)) - self.modules[name] = co, pyc - return self - - def _early_rewrite_bailout(self, name, state): - """ - This is a fast way to get out of rewriting modules. Profiling has - shown that the call to imp.find_module (inside of the find_module - 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 - for path in self.session._initialpaths: - # Make something as c:/projects/my_project/path.py -> - # ['c:', 'projects', 'my_project', 'path.py'] - parts = str(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 - - state.trace("early skip of rewriting module: %s" % (name,)) - return True - - def _should_rewrite(self, name, fn_pypath, state): - # always rewrite conftest files - fn = str(fn_pypath) - if fn_pypath.basename == "conftest.py": - state.trace("rewriting conftest file: %r" % (fn,)) - return True - - if self.session is not None: - if self.session.isinitpath(fn): - state.trace("matched test file (was specified on cmdline): %r" % (fn,)) - return True - - # modules not passed explicitly on the command line are only - # rewritten if they match the naming convention for test files - for pat in self.fnpats: - if fn_pypath.fnmatch(pat): - state.trace("matched test file %r" % (fn,)) - return True - - return self._is_marked_for_rewrite(name, state) - - def _is_marked_for_rewrite(self, name, state): - 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("matched marked file %r (from %r)" % (name, marked)) - self._marked_for_rewrite_cache[name] = True - return True - - self._marked_for_rewrite_cache[name] = False - return False - - def mark_rewrite(self, *names): - """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: - if not AssertionRewriter.is_rewrite_disabled( - sys.modules[name].__doc__ or "" - ): - self._warn_already_imported(name) - self._must_rewrite.update(names) - self._marked_for_rewrite_cache.clear() - - def _warn_already_imported(self, name): + self.session = None + self.modules = {} + self._rewritten_names = set() + self._must_rewrite = 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"} + self._marked_for_rewrite_cache = {} + self._session_paths_checked = False + + def set_session(self, session): + self.session = session + self._session_paths_checked = False + + def _imp_find_module(self, name, path=None): + """Indirection so we can mock calls to find_module originated from the hook during testing""" + return imp.find_module(name, path) + + def find_module(self, name, path=None): + if self._writing_pyc: + return None + state = self.config._assertstate + if self._early_rewrite_bailout(name, state): + return None + state.trace("find_module called for: %s" % name) + names = name.rsplit(".", 1) + lastname = names[-1] + pth = None + if path is not None: + # Starting with Python 3.3, path is a _NamespacePath(), which + # causes problems if not converted to list. + path = list(path) + if len(path) == 1: + pth = path[0] + if pth is None: + try: + fd, fn, desc = self._imp_find_module(lastname, path) + except ImportError: + return None + if fd is not None: + fd.close() + tp = desc[2] + if tp == imp.PY_COMPILED: + if hasattr(imp, "source_from_cache"): + try: + fn = imp.source_from_cache(fn) + except ValueError: + # Python 3 doesn't like orphaned but still-importable + # .pyc files. + fn = fn[:-1] + else: + fn = fn[:-1] + elif tp != imp.PY_SOURCE: + # Don't know what this is. + return None + else: + fn = os.path.join(pth, name.rpartition(".")[2] + ".py") + + fn_pypath = py.path.local(fn) + if not self._should_rewrite(name, fn_pypath, state): + return None + + self._rewritten_names.add(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 + cache_dir = os.path.join(fn_pypath.dirname, "__pycache__") + if write: + try: + os.mkdir(cache_dir) + except OSError: + e = sys.exc_info()[1].errno + if e == errno.EEXIST: + # Either the __pycache__ directory already exists (the + # common case) or it's blocked by a non-dir node. In the + # latter case, we'll ignore it in _write_pyc. + pass + elif e in [errno.ENOENT, errno.ENOTDIR]: + # One of the path components was not a directory, likely + # because we're in a zip file. + write = False + elif e in [errno.EACCES, errno.EROFS, errno.EPERM]: + state.trace("read only directory: %r" % fn_pypath.dirname) + write = False + else: + raise + cache_name = fn_pypath.basename[:-3] + PYC_TAIL + pyc = os.path.join(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... + co = _read_pyc(fn_pypath, pyc, state.trace) + if co is None: + state.trace("rewriting %r" % (fn,)) + source_stat, co = _rewrite_test(self.config, fn_pypath) + if co is None: + # Probably a SyntaxError in the test. + return None + if write: + self._writing_pyc = True + try: + _write_pyc(state, co, source_stat, pyc) + finally: + self._writing_pyc = False + else: + state.trace("found cached rewritten pyc for %r" % (fn,)) + self.modules[name] = co, pyc + return self + + def _early_rewrite_bailout(self, name, state): + """ + This is a fast way to get out of rewriting modules. Profiling has + shown that the call to imp.find_module (inside of the find_module + 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 + for path in self.session._initialpaths: + # Make something as c:/projects/my_project/path.py -> + # ['c:', 'projects', 'my_project', 'path.py'] + parts = str(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 + + state.trace("early skip of rewriting module: %s" % (name,)) + return True + + def _should_rewrite(self, name, fn_pypath, state): + # always rewrite conftest files + fn = str(fn_pypath) + if fn_pypath.basename == "conftest.py": + state.trace("rewriting conftest file: %r" % (fn,)) + return True + + if self.session is not None: + if self.session.isinitpath(fn): + state.trace("matched test file (was specified on cmdline): %r" % (fn,)) + return True + + # modules not passed explicitly on the command line are only + # rewritten if they match the naming convention for test files + for pat in self.fnpats: + if fn_pypath.fnmatch(pat): + state.trace("matched test file %r" % (fn,)) + return True + + return self._is_marked_for_rewrite(name, state) + + def _is_marked_for_rewrite(self, name, state): + 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("matched marked file %r (from %r)" % (name, marked)) + self._marked_for_rewrite_cache[name] = True + return True + + self._marked_for_rewrite_cache[name] = False + return False + + def mark_rewrite(self, *names): + """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: + if not AssertionRewriter.is_rewrite_disabled( + sys.modules[name].__doc__ or "" + ): + self._warn_already_imported(name) + self._must_rewrite.update(names) + self._marked_for_rewrite_cache.clear() + + def _warn_already_imported(self, name): from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warnings import _issue_warning_captured - + _issue_warning_captured( PytestAssertRewriteWarning( "Module already imported so cannot be rewritten: %s" % name ), self.config.hook, - stacklevel=5, - ) - - def load_module(self, name): - co, pyc = self.modules.pop(name) - if name in sys.modules: - # If there is an existing module object named 'fullname' in - # sys.modules, the loader must use that existing module. (Otherwise, - # the reload() builtin will not work correctly.) - mod = sys.modules[name] - else: - # I wish I could just call imp.load_compiled here, but __file__ has to - # be set properly. In Python 3.2+, this all would be handled correctly - # by load_compiled. - mod = sys.modules[name] = imp.new_module(name) - try: - mod.__file__ = co.co_filename - # Normally, this attribute is 3.2+. - mod.__cached__ = pyc - mod.__loader__ = self - # Normally, this attribute is 3.4+ - mod.__spec__ = spec_from_file_location(name, co.co_filename, loader=self) + stacklevel=5, + ) + + def load_module(self, name): + co, pyc = self.modules.pop(name) + if name in sys.modules: + # If there is an existing module object named 'fullname' in + # sys.modules, the loader must use that existing module. (Otherwise, + # the reload() builtin will not work correctly.) + mod = sys.modules[name] + else: + # I wish I could just call imp.load_compiled here, but __file__ has to + # be set properly. In Python 3.2+, this all would be handled correctly + # by load_compiled. + mod = sys.modules[name] = imp.new_module(name) + try: + mod.__file__ = co.co_filename + # Normally, this attribute is 3.2+. + mod.__cached__ = pyc + mod.__loader__ = self + # Normally, this attribute is 3.4+ + mod.__spec__ = spec_from_file_location(name, co.co_filename, loader=self) exec(co, mod.__dict__) - except: # noqa - if name in sys.modules: - del sys.modules[name] - raise - return sys.modules[name] - - def is_package(self, name): - try: - fd, fn, desc = self._imp_find_module(name) - except ImportError: - return False - if fd is not None: - fd.close() - tp = desc[2] - return tp == imp.PKG_DIRECTORY - - def get_data(self, pathname): - """Optional PEP302 get_data API. - """ - with open(pathname, "rb") as f: - return f.read() - - -def _write_pyc(state, co, source_stat, pyc): - # 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 deviate, and I hope - # sometime to be able to use imp.load_compiled to load them. (See - # the comment in load_module above.) - try: - with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp: - fp.write(imp.get_magic()) + except: # noqa + if name in sys.modules: + del sys.modules[name] + raise + return sys.modules[name] + + def is_package(self, name): + try: + fd, fn, desc = self._imp_find_module(name) + except ImportError: + return False + if fd is not None: + fd.close() + tp = desc[2] + return tp == imp.PKG_DIRECTORY + + def get_data(self, pathname): + """Optional PEP302 get_data API. + """ + with open(pathname, "rb") as f: + return f.read() + + +def _write_pyc(state, co, source_stat, pyc): + # 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 deviate, and I hope + # sometime to be able to use imp.load_compiled to load them. (See + # the comment in load_module above.) + try: + with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp: + fp.write(imp.get_magic()) # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) mtime = int(source_stat.mtime) & 0xFFFFFFFF - size = source_stat.size & 0xFFFFFFFF + size = source_stat.size & 0xFFFFFFFF # "<LL" stands for 2 unsigned longs, little-ending fp.write(struct.pack("<LL", mtime, size)) - fp.write(marshal.dumps(co)) - except EnvironmentError as e: - state.trace("error writing pyc file at %s: errno=%s" % (pyc, e.errno)) - # we ignore any failure to write the cache file - # there are many reasons, permission-denied, __pycache__ being a - # file etc. - return False - return True - - -RN = "\r\n".encode("utf-8") -N = "\n".encode("utf-8") - -cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+") -BOM_UTF8 = "\xef\xbb\xbf" - - -def _rewrite_test(config, fn): - """Try to read and rewrite *fn* and return the code object.""" - state = config._assertstate - try: - stat = fn.stat() - source = fn.read("rb") - except EnvironmentError: - return None, None - if ASCII_IS_DEFAULT_ENCODING: - # ASCII is the default encoding in Python 2. Without a coding - # declaration, Python 2 will complain about any bytes in the file - # outside the ASCII range. Sadly, this behavior does not extend to - # compile() or ast.parse(), which prefer to interpret the bytes as - # latin-1. (At least they properly handle explicit coding cookies.) To - # preserve this error behavior, we could force ast.parse() to use ASCII - # as the encoding by inserting a coding cookie. Unfortunately, that - # messes up line numbers. Thus, we have to check ourselves if anything - # is outside the ASCII range in the case no encoding is explicitly - # declared. For more context, see issue #269. Yay for Python 3 which - # gets this right. - end1 = source.find("\n") - end2 = source.find("\n", end1 + 1) - if ( - not source.startswith(BOM_UTF8) - and cookie_re.match(source[0:end1]) is None - and cookie_re.match(source[end1 + 1 : end2]) is None - ): - if hasattr(state, "_indecode"): - # encodings imported us again, so don't rewrite. - return None, None - state._indecode = True - try: - try: - source.decode("ascii") - except UnicodeDecodeError: - # Let it fail in real import. - return None, None - finally: - del state._indecode - try: - tree = ast.parse(source, filename=fn.strpath) - except SyntaxError: - # Let this pop up again in the real import. - state.trace("failed to parse: %r" % (fn,)) - return None, None - rewrite_asserts(tree, fn, config) - try: - co = compile(tree, fn.strpath, "exec", dont_inherit=True) - except SyntaxError: - # It's possible that this error is from some bug in the - # assertion rewriting, but I don't know of a fast way to tell. - state.trace("failed to compile: %r" % (fn,)) - return None, None - return stat, co - - -def _read_pyc(source, pyc, trace=lambda x: None): - """Possibly read a pytest pyc containing rewritten code. - - Return rewritten code if successful or None if not. - """ - try: - fp = open(pyc, "rb") - except IOError: - return None - with fp: - try: - mtime = int(source.mtime()) - size = source.size() - data = fp.read(12) - except EnvironmentError as e: - trace("_read_pyc(%s): EnvironmentError %s" % (source, e)) - return None - # Check for invalid or out of date pyc file. - if ( - len(data) != 12 - or data[:4] != imp.get_magic() + fp.write(marshal.dumps(co)) + except EnvironmentError as e: + state.trace("error writing pyc file at %s: errno=%s" % (pyc, e.errno)) + # we ignore any failure to write the cache file + # there are many reasons, permission-denied, __pycache__ being a + # file etc. + return False + return True + + +RN = "\r\n".encode("utf-8") +N = "\n".encode("utf-8") + +cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+") +BOM_UTF8 = "\xef\xbb\xbf" + + +def _rewrite_test(config, fn): + """Try to read and rewrite *fn* and return the code object.""" + state = config._assertstate + try: + stat = fn.stat() + source = fn.read("rb") + except EnvironmentError: + return None, None + if ASCII_IS_DEFAULT_ENCODING: + # ASCII is the default encoding in Python 2. Without a coding + # declaration, Python 2 will complain about any bytes in the file + # outside the ASCII range. Sadly, this behavior does not extend to + # compile() or ast.parse(), which prefer to interpret the bytes as + # latin-1. (At least they properly handle explicit coding cookies.) To + # preserve this error behavior, we could force ast.parse() to use ASCII + # as the encoding by inserting a coding cookie. Unfortunately, that + # messes up line numbers. Thus, we have to check ourselves if anything + # is outside the ASCII range in the case no encoding is explicitly + # declared. For more context, see issue #269. Yay for Python 3 which + # gets this right. + end1 = source.find("\n") + end2 = source.find("\n", end1 + 1) + if ( + not source.startswith(BOM_UTF8) + and cookie_re.match(source[0:end1]) is None + and cookie_re.match(source[end1 + 1 : end2]) is None + ): + if hasattr(state, "_indecode"): + # encodings imported us again, so don't rewrite. + return None, None + state._indecode = True + try: + try: + source.decode("ascii") + except UnicodeDecodeError: + # Let it fail in real import. + return None, None + finally: + del state._indecode + try: + tree = ast.parse(source, filename=fn.strpath) + except SyntaxError: + # Let this pop up again in the real import. + state.trace("failed to parse: %r" % (fn,)) + return None, None + rewrite_asserts(tree, fn, config) + try: + co = compile(tree, fn.strpath, "exec", dont_inherit=True) + except SyntaxError: + # It's possible that this error is from some bug in the + # assertion rewriting, but I don't know of a fast way to tell. + state.trace("failed to compile: %r" % (fn,)) + return None, None + return stat, co + + +def _read_pyc(source, pyc, trace=lambda x: None): + """Possibly read a pytest pyc containing rewritten code. + + Return rewritten code if successful or None if not. + """ + try: + fp = open(pyc, "rb") + except IOError: + return None + with fp: + try: + mtime = int(source.mtime()) + size = source.size() + data = fp.read(12) + except EnvironmentError as e: + trace("_read_pyc(%s): EnvironmentError %s" % (source, e)) + return None + # Check for invalid or out of date pyc file. + if ( + len(data) != 12 + or data[:4] != imp.get_magic() or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF) - ): - trace("_read_pyc(%s): invalid or out of date pyc" % source) - return None - try: - co = marshal.load(fp) - except Exception as e: - trace("_read_pyc(%s): marshal.load error %s" % (source, e)) - 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, module_path=None, config=None): - """Rewrite the assert statements in mod.""" - AssertionRewriter(module_path, config).run(mod) - - -def _saferepr(obj): - """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. - - """ + ): + trace("_read_pyc(%s): invalid or out of date pyc" % source) + return None + try: + co = marshal.load(fp) + except Exception as e: + trace("_read_pyc(%s): marshal.load error %s" % (source, e)) + 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, module_path=None, config=None): + """Rewrite the assert statements in mod.""" + AssertionRewriter(module_path, config).run(mod) + + +def _saferepr(obj): + """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. + + """ r = saferepr(obj) - # only occurs in python2.x, repr must return text in python3+ - if isinstance(r, bytes): - # Represent unprintable bytes as `\x##` - r = u"".join( - u"\\x{:x}".format(ord(c)) if c not in string.printable else c.decode() - for c in r - ) - return r.replace(u"\n", u"\\n") - - -def _format_assertmsg(obj): - """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 + # only occurs in python2.x, repr must return text in python3+ + if isinstance(r, bytes): + # Represent unprintable bytes as `\x##` + r = u"".join( + u"\\x{:x}".format(ord(c)) if c not in string.printable else c.decode() + for c in r + ) + return r.replace(u"\n", u"\\n") + + +def _format_assertmsg(obj): + """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 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. - replaces = [(u"\n", u"\n~"), (u"%", u"%%")] - if not isinstance(obj, six.string_types): + + """ + # 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 = [(u"\n", u"\n~"), (u"%", u"%%")] + if not isinstance(obj, six.string_types): obj = saferepr(obj) - replaces.append((u"\\n", u"\n~")) - - if isinstance(obj, bytes): - replaces = [(r1.encode(), r2.encode()) for r1, r2 in replaces] - - for r1, r2 in replaces: - obj = obj.replace(r1, r2) - - return obj - - -def _should_repr_global_name(obj): + replaces.append((u"\\n", u"\n~")) + + if isinstance(obj, bytes): + replaces = [(r1.encode(), r2.encode()) for r1, r2 in replaces] + + for r1, r2 in replaces: + obj = obj.replace(r1, r2) + + return obj + + +def _should_repr_global_name(obj): if callable(obj): return False - + try: return not hasattr(obj, "__name__") except Exception: return True - - -def _format_boolop(explanations, is_or): - explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" - if isinstance(explanation, six.text_type): - return explanation.replace(u"%", u"%%") - else: - return explanation.replace(b"%", b"%%") - - -def _call_reprcompare(ops, results, expls, each_obj): - 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 - - -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", -} -# Python 3.5+ compatibility -try: - binop_map[ast.MatMult] = "@" -except AttributeError: - pass - -# Python 3.4+ compatibility -if hasattr(ast, "NameConstant"): - _NameConstant = ast.NameConstant -else: - - def _NameConstant(c): - return ast.Name(str(c), ast.Load()) - - -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 - - -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. - - 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". - - :on_failure: The AST statements which will be executed if the - assertion test fails. This is the code which will construct - the failure message and raises the AssertionError. - - :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, config): - super(AssertionRewriter, self).__init__() - self.module_path = module_path - self.config = config - - def run(self, mod): - """Find all assert statements in *mod* and rewrite them.""" - if not mod.body: - # Nothing to do. - return - # Insert some special imports at the top of the module but after any - # docstrings and __future__ imports. - aliases = [ + + +def _format_boolop(explanations, is_or): + explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" + if isinstance(explanation, six.text_type): + return explanation.replace(u"%", u"%%") + else: + return explanation.replace(b"%", b"%%") + + +def _call_reprcompare(ops, results, expls, each_obj): + 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 + + +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", +} +# Python 3.5+ compatibility +try: + binop_map[ast.MatMult] = "@" +except AttributeError: + pass + +# Python 3.4+ compatibility +if hasattr(ast, "NameConstant"): + _NameConstant = ast.NameConstant +else: + + def _NameConstant(c): + return ast.Name(str(c), ast.Load()) + + +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 + + +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. + + 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". + + :on_failure: The AST statements which will be executed if the + assertion test fails. This is the code which will construct + the failure message and raises the AssertionError. + + :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, config): + super(AssertionRewriter, self).__init__() + self.module_path = module_path + self.config = config + + def run(self, mod): + """Find all assert statements in *mod* and rewrite them.""" + if not mod.body: + # Nothing to do. + return + # Insert some special imports at the top of the module but after any + # docstrings and __future__ imports. + aliases = [ ast.alias(six.moves.builtins.__name__, "@py_builtins"), - ast.alias("_pytest.assertion.rewrite", "@pytest_ar"), - ] - 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 ( - not isinstance(item, ast.ImportFrom) - or item.level > 0 - or item.module != "__future__" - ): - lineno = item.lineno - break - pos += 1 - else: - lineno = item.lineno - imports = [ - ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases - ] - mod.body[pos:pos] = imports - # Collect asserts. - nodes = [mod] - while nodes: - node = nodes.pop() - for name, field in ast.iter_fields(node): - if isinstance(field, list): - new = [] - 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): - return "PYTEST_DONT_REWRITE" in docstring - - def variable(self): - """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): - """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.alias("_pytest.assertion.rewrite", "@pytest_ar"), + ] + 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 ( + not isinstance(item, ast.ImportFrom) + or item.level > 0 + or item.module != "__future__" + ): + lineno = item.lineno + break + pos += 1 + else: + lineno = item.lineno + imports = [ + ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases + ] + mod.body[pos:pos] = imports + # Collect asserts. + nodes = [mod] + while nodes: + node = nodes.pop() + for name, field in ast.iter_fields(node): + if isinstance(field, list): + new = [] + 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): + return "PYTEST_DONT_REWRITE" in docstring + + def variable(self): + """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): + """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): """Call saferepr on the expression.""" return self.helper("_saferepr", expr) - - def helper(self, name, *args): - """Call a helper in this module.""" - py_name = ast.Name("@pytest_ar", ast.Load()) + + def helper(self, name, *args): + """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): - """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): - """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): - """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 = {} - self.stack.append(self.explanation_specifiers) - - def pop_format_context(self, expl_expr): - """Format the %-formatted string with current format context. - - The expl_expr should be an ast.Str instance constructed from - the %-placeholders created by .explanation_param(). This will - add the required code to format said string to .on_failure 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)) - self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) - return ast.Name(name, ast.Load()) - - def generic_visit(self, node): - """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_): - """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 ast_Call(attr, list(args), []) + + def builtin(self, name): + """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): + """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): + """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 = {} + self.stack.append(self.explanation_specifiers) + + def pop_format_context(self, expl_expr): + """Format the %-formatted string with current format context. + + The expl_expr should be an ast.Str instance constructed from + the %-placeholders created by .explanation_param(). This will + add the required code to format said string to .on_failure 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)) + self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form)) + return ast.Name(name, ast.Load()) + + def generic_visit(self, node): + """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_): + """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 - - warnings.warn_explicit( + import warnings + + warnings.warn_explicit( PytestAssertRewriteWarning( "assertion is always true, perhaps remove parentheses?" ), - category=None, - filename=str(self.module_path), - lineno=assert_.lineno, - ) - - self.statements = [] - self.variables = [] - self.variable_counter = itertools.count() - self.stack = [] - self.on_failure = [] - self.push_format_context() - # Rewrite assert into a bunch of statements. - top_condition, explanation = self.visit(assert_.test) + category=None, + filename=str(self.module_path), + lineno=assert_.lineno, + ) + + self.statements = [] + self.variables = [] + self.variable_counter = itertools.count() + self.stack = [] + self.on_failure = [] + self.push_format_context() + # Rewrite assert into a bunch of statements. + top_condition, explanation = self.visit(assert_.test) # If in a test module, check if directly asserting None, in order to warn [Issue #3191] if self.module_path is not None: self.statements.append( @@ -836,36 +836,36 @@ class AssertionRewriter(ast.NodeVisitor): top_condition, module_path=self.module_path, lineno=assert_.lineno ) ) - # Create failure message. - body = self.on_failure - negation = ast.UnaryOp(ast.Not(), top_condition) - self.statements.append(ast.If(negation, body, [])) - if assert_.msg: + # Create failure message. + body = self.on_failure + negation = ast.UnaryOp(ast.Not(), top_condition) + self.statements.append(ast.If(negation, body, [])) + if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) - explanation = "\n>assert " + explanation - else: - assertmsg = ast.Str("") - explanation = "assert " + explanation - template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) - msg = self.pop_format_context(template) + explanation = "\n>assert " + explanation + else: + assertmsg = ast.Str("") + explanation = "assert " + explanation + template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) + msg = self.pop_format_context(template) fmt = self.helper("_format_explanation", msg) - err_name = ast.Name("AssertionError", ast.Load()) - exc = ast_Call(err_name, [fmt], []) - if sys.version_info[0] >= 3: - raise_ = ast.Raise(exc, None) - else: - raise_ = ast.Raise(exc, None, 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 = ast.Assign(variables, _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 - + err_name = ast.Name("AssertionError", ast.Load()) + exc = ast_Call(err_name, [fmt], []) + if sys.version_info[0] >= 3: + raise_ = ast.Raise(exc, None) + else: + raise_ = ast.Raise(exc, None, 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 = ast.Assign(variables, _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 + def warn_about_none_ast(self, node, module_path, lineno): """ Returns an AST issuing a warning if the value of node is `None`. @@ -893,180 +893,180 @@ warn_explicit( ).body return ast.If(val_is_none, send_warning, []) - def visit_Name(self, name): - # 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]) + def visit_Name(self, name): + # 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]) 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) - - def visit_BoolOp(self, boolop): - 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.on_failure - levels = len(boolop.values) - 1 - self.push_format_context() - # Process each operand, short-circuting if needed. - for i, v in enumerate(boolop.values): - if i: - fail_inner = [] - # cond is set in a prior loop iteration below - self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa - self.on_failure = 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)) - call = ast_Call(app, [expl_format], []) - self.on_failure.append(ast.Expr(call)) - if i < levels: - cond = res - if is_or: - cond = ast.UnaryOp(ast.Not(), cond) - inner = [] - self.statements.append(ast.If(cond, inner, [])) - self.statements = body = inner - self.statements = save - self.on_failure = fail_save + 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): + 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.on_failure + levels = len(boolop.values) - 1 + self.push_format_context() + # Process each operand, short-circuting if needed. + for i, v in enumerate(boolop.values): + if i: + fail_inner = [] + # cond is set in a prior loop iteration below + self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa + self.on_failure = 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)) + call = ast_Call(app, [expl_format], []) + self.on_failure.append(ast.Expr(call)) + if i < levels: + cond = res + if is_or: + cond = ast.UnaryOp(ast.Not(), cond) + inner = [] + self.statements.append(ast.If(cond, inner, [])) + self.statements = body = inner + self.statements = save + self.on_failure = 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) - - def visit_UnaryOp(self, unary): - 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,) - - def visit_BinOp(self, binop): - symbol = binop_map[binop.op.__class__] - left_expr, left_expl = self.visit(binop.left) - right_expr, right_expl = self.visit(binop.right) - explanation = "(%s %s %s)" % (left_expl, symbol, right_expl) - res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) - return res, explanation - - def visit_Call_35(self, call): - """ - visit `ast.Call` nodes on Python3.5 and after - """ - 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 = "%s(%s)" % (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)) - outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) - return res, outer_expl - - def visit_Starred(self, starred): - # From Python 3.5, 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 - - def visit_Call_legacy(self, call): - """ - visit `ast.Call nodes on 3.4 and below` - """ - new_func, func_expl = self.visit(call.func) - arg_expls = [] - new_args = [] - new_kwargs = [] - new_star = new_kwarg = None - for arg in call.args: - res, expl = self.visit(arg) - new_args.append(res) - arg_expls.append(expl) - for keyword in call.keywords: - res, expl = self.visit(keyword.value) - new_kwargs.append(ast.keyword(keyword.arg, res)) - arg_expls.append(keyword.arg + "=" + expl) - if call.starargs: - new_star, expl = self.visit(call.starargs) - arg_expls.append("*" + expl) - if call.kwargs: - new_kwarg, expl = self.visit(call.kwargs) - arg_expls.append("**" + expl) - expl = "%s(%s)" % (func_expl, ", ".join(arg_expls)) - new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg) - res = self.assign(new_call) - res_expl = self.explanation_param(self.display(res)) - outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) - return res, outer_expl - - # ast.Call signature changed on 3.5, - # conditionally change which methods is named - # visit_Call depending on Python version - if sys.version_info >= (3, 5): - visit_Call = visit_Call_35 - else: - visit_Call = visit_Call_legacy - - def visit_Attribute(self, attr): - 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): - self.push_format_context() - left_res, left_expl = self.visit(comp.left) - if isinstance(comp.left, (ast.Compare, ast.BoolOp)): - left_expl = "({})".format(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)): - next_expl = "({})".format(next_expl) - results.append(next_res) - sym = binop_map[op.__class__] - syms.append(ast.Str(sym)) - expl = "%s %s %s" % (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( + expl = self.pop_format_context(expl_template) + return ast.Name(res_var, ast.Load()), self.explanation_param(expl) + + def visit_UnaryOp(self, unary): + 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,) + + def visit_BinOp(self, binop): + symbol = binop_map[binop.op.__class__] + left_expr, left_expl = self.visit(binop.left) + right_expr, right_expl = self.visit(binop.right) + explanation = "(%s %s %s)" % (left_expl, symbol, right_expl) + res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) + return res, explanation + + def visit_Call_35(self, call): + """ + visit `ast.Call` nodes on Python3.5 and after + """ + 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 = "%s(%s)" % (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)) + outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) + return res, outer_expl + + def visit_Starred(self, starred): + # From Python 3.5, 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 + + def visit_Call_legacy(self, call): + """ + visit `ast.Call nodes on 3.4 and below` + """ + new_func, func_expl = self.visit(call.func) + arg_expls = [] + new_args = [] + new_kwargs = [] + new_star = new_kwarg = None + for arg in call.args: + res, expl = self.visit(arg) + new_args.append(res) + arg_expls.append(expl) + for keyword in call.keywords: + res, expl = self.visit(keyword.value) + new_kwargs.append(ast.keyword(keyword.arg, res)) + arg_expls.append(keyword.arg + "=" + expl) + if call.starargs: + new_star, expl = self.visit(call.starargs) + arg_expls.append("*" + expl) + if call.kwargs: + new_kwarg, expl = self.visit(call.kwargs) + arg_expls.append("**" + expl) + expl = "%s(%s)" % (func_expl, ", ".join(arg_expls)) + new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg) + res = self.assign(new_call) + res_expl = self.explanation_param(self.display(res)) + outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) + return res, outer_expl + + # ast.Call signature changed on 3.5, + # conditionally change which methods is named + # visit_Call depending on Python version + if sys.version_info >= (3, 5): + visit_Call = visit_Call_35 + else: + visit_Call = visit_Call_legacy + + def visit_Attribute(self, attr): + 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): + self.push_format_context() + left_res, left_expl = self.visit(comp.left) + if isinstance(comp.left, (ast.Compare, ast.BoolOp)): + left_expl = "({})".format(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)): + next_expl = "({})".format(next_expl) + results.append(next_res) + sym = binop_map[op.__class__] + syms.append(ast.Str(sym)) + expl = "%s %s %s" % (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( "_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: - res = ast.BoolOp(ast.And(), load_names) - else: - res = load_names[0] - return res, self.explanation_param(self.pop_format_context(expl_call)) + 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.BoolOp(ast.And(), load_names) + else: + res = load_names[0] + return res, self.explanation_param(self.pop_format_context(expl_call)) diff --git a/contrib/python/pytest/py2/_pytest/assertion/truncate.py b/contrib/python/pytest/py2/_pytest/assertion/truncate.py index 9412b168d2..525896ea9a 100644 --- a/contrib/python/pytest/py2/_pytest/assertion/truncate.py +++ b/contrib/python/pytest/py2/_pytest/assertion/truncate.py @@ -1,102 +1,102 @@ # -*- coding: utf-8 -*- -""" -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. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os - -import six - -DEFAULT_MAX_LINES = 8 -DEFAULT_MAX_CHARS = 8 * 80 -USAGE_MSG = "use '-vv' to show" - - -def truncate_if_required(explanation, item, max_length=None): - """ - Truncate this assertion explanation if the given test item is eligible. - """ - if _should_truncate_item(item): - return _truncate_explanation(explanation) - return explanation - - -def _should_truncate_item(item): - """ - Whether or not this test item is eligible for truncation. - """ - verbose = item.config.option.verbose - return verbose < 2 and not _running_on_ci() - - -def _running_on_ci(): - """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, max_lines=None, max_chars=None): - """ - 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: - msg += " ({} line hidden)".format(truncated_line_count) - else: - msg += " ({} lines hidden)".format(truncated_line_count) - msg += ", {}".format(USAGE_MSG) - truncated_explanation.extend([six.text_type(""), six.text_type(msg)]) - return truncated_explanation - - -def _truncate_by_char_count(input_lines, max_chars): - # 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 +""" +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. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +import six + +DEFAULT_MAX_LINES = 8 +DEFAULT_MAX_CHARS = 8 * 80 +USAGE_MSG = "use '-vv' to show" + + +def truncate_if_required(explanation, item, max_length=None): + """ + Truncate this assertion explanation if the given test item is eligible. + """ + if _should_truncate_item(item): + return _truncate_explanation(explanation) + return explanation + + +def _should_truncate_item(item): + """ + Whether or not this test item is eligible for truncation. + """ + verbose = item.config.option.verbose + return verbose < 2 and not _running_on_ci() + + +def _running_on_ci(): + """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, max_lines=None, max_chars=None): + """ + 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: + msg += " ({} line hidden)".format(truncated_line_count) + else: + msg += " ({} lines hidden)".format(truncated_line_count) + msg += ", {}".format(USAGE_MSG) + truncated_explanation.extend([six.text_type(""), six.text_type(msg)]) + return truncated_explanation + + +def _truncate_by_char_count(input_lines, max_chars): + # 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/py2/_pytest/assertion/util.py b/contrib/python/pytest/py2/_pytest/assertion/util.py index 6d63b0f9d4..c382f1c609 100644 --- a/contrib/python/pytest/py2/_pytest/assertion/util.py +++ b/contrib/python/pytest/py2/_pytest/assertion/util.py @@ -1,125 +1,125 @@ # -*- coding: utf-8 -*- -"""Utilities for assertion debugging""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import pprint - -import six - -import _pytest._code -from ..compat import Sequence +"""Utilities for assertion debugging""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import pprint + +import six + +import _pytest._code +from ..compat import Sequence from _pytest import outcomes from _pytest._io.saferepr import saferepr from _pytest.compat import ATTRS_EQ_FIELD - -# 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 = None - - -# the re-encoding is needed for python2 repr -# with non-ascii characters (see issue 877 and 1379) -def ecu(s): - if isinstance(s, bytes): - return s.decode("UTF-8", "replace") - else: - return s - - -def format_explanation(explanation): - """This formats 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. - """ - explanation = ecu(explanation) - lines = _split_explanation(explanation) - result = _format_lines(lines) - return u"\n".join(result) - - -def _split_explanation(explanation): - """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. - """ - raw_lines = (explanation or u"").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 - - -def _format_lines(lines): - """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. - """ - result = lines[:1] - stack = [0] - stackcnt = [0] - for line in lines[1:]: - if line.startswith("{"): - if stackcnt[-1]: - s = u"and " - else: - s = u"where " - stack.append(len(result)) - stackcnt[-1] += 1 - stackcnt.append(0) - result.append(u" +" + u" " * (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 - result.append(u" " * indent + line[1:]) - assert len(stack) == 1 - return result - - -# Provide basestring in python3 -try: - basestring = basestring -except NameError: - basestring = str - - + +# 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 = None + + +# the re-encoding is needed for python2 repr +# with non-ascii characters (see issue 877 and 1379) +def ecu(s): + if isinstance(s, bytes): + return s.decode("UTF-8", "replace") + else: + return s + + +def format_explanation(explanation): + """This formats 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. + """ + explanation = ecu(explanation) + lines = _split_explanation(explanation) + result = _format_lines(lines) + return u"\n".join(result) + + +def _split_explanation(explanation): + """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. + """ + raw_lines = (explanation or u"").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 + + +def _format_lines(lines): + """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. + """ + result = lines[:1] + stack = [0] + stackcnt = [0] + for line in lines[1:]: + if line.startswith("{"): + if stackcnt[-1]: + s = u"and " + else: + s = u"where " + stack.append(len(result)) + stackcnt[-1] += 1 + stackcnt.append(0) + result.append(u" +" + u" " * (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 + result.append(u" " * indent + line[1:]) + assert len(stack) == 1 + return result + + +# Provide basestring in python3 +try: + basestring = basestring +except NameError: + basestring = str + + def issequence(x): return isinstance(x, Sequence) and not isinstance(x, basestring) - - + + def istext(x): return isinstance(x, basestring) - - + + def isdict(x): return isinstance(x, dict) - - + + def isset(x): return isinstance(x, (set, frozenset)) - + def isdatacls(obj): return getattr(obj, "__dataclass_fields__", None) is not None @@ -145,111 +145,111 @@ def assertrepr_compare(config, op, left, right): summary = u"%s %s %s" % (ecu(left_repr), op, ecu(right_repr)) - verbose = config.getoption("verbose") - explanation = None - try: - if op == "==": - if istext(left) and istext(right): - explanation = _diff_text(left, right, verbose) - else: - if issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right, verbose) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right, verbose) - elif isdict(left) and isdict(right): - explanation = _compare_eq_dict(left, right, verbose) + verbose = config.getoption("verbose") + explanation = None + try: + if op == "==": + if istext(left) and istext(right): + explanation = _diff_text(left, right, verbose) + else: + if issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right, verbose) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right, verbose) + elif isdict(left) and isdict(right): + explanation = _compare_eq_dict(left, right, verbose) elif type(left) == type(right) and (isdatacls(left) or isattrs(left)): type_fn = (isdatacls, isattrs) explanation = _compare_eq_cls(left, right, verbose, type_fn) elif verbose > 0: explanation = _compare_eq_verbose(left, right) - if isiterable(left) and isiterable(right): - expl = _compare_eq_iterable(left, right, verbose) - if explanation is not None: - explanation.extend(expl) - else: - explanation = expl - elif op == "not in": - if istext(left) and istext(right): - explanation = _notin_text(left, right, verbose) + if isiterable(left) and isiterable(right): + expl = _compare_eq_iterable(left, right, verbose) + if explanation is not None: + explanation.extend(expl) + else: + explanation = expl + elif op == "not in": + if istext(left) and istext(right): + explanation = _notin_text(left, right, verbose) except outcomes.Exit: raise - except Exception: - explanation = [ - u"(pytest_assertion plugin: representation of details failed. " - u"Probably an object has a faulty __repr__.)", + except Exception: + explanation = [ + u"(pytest_assertion plugin: representation of details failed. " + u"Probably an object has a faulty __repr__.)", six.text_type(_pytest._code.ExceptionInfo.from_current()), - ] - - if not explanation: - return None - - return [summary] + explanation - - + ] + + if not explanation: + return None + + return [summary] + explanation + + def _diff_text(left, right, verbose=0): """Return the explanation for the diff between text or bytes. - - Unless --verbose is used this will skip leading and trailing - characters which are identical to keep the diff minimal. - - If the input are bytes they will be safely converted to text. - """ - from difflib import ndiff - - explanation = [] - - def escape_for_readable_diff(binary_text): - """ - Ensures that the internal string is always valid unicode, converting any bytes safely to valid unicode. - This is done using repr() which then needs post-processing to fix the encompassing quotes and un-escape - newlines and carriage returns (#429). - """ - r = six.text_type(repr(binary_text)[1:-1]) - r = r.replace(r"\n", "\n") - r = r.replace(r"\r", "\r") - return r - - if isinstance(left, bytes): - left = escape_for_readable_diff(left) - if isinstance(right, bytes): - right = escape_for_readable_diff(right) + + Unless --verbose is used this will skip leading and trailing + characters which are identical to keep the diff minimal. + + If the input are bytes they will be safely converted to text. + """ + from difflib import ndiff + + explanation = [] + + def escape_for_readable_diff(binary_text): + """ + Ensures that the internal string is always valid unicode, converting any bytes safely to valid unicode. + This is done using repr() which then needs post-processing to fix the encompassing quotes and un-escape + newlines and carriage returns (#429). + """ + r = six.text_type(repr(binary_text)[1:-1]) + r = r.replace(r"\n", "\n") + r = r.replace(r"\r", "\r") + return r + + if isinstance(left, bytes): + left = escape_for_readable_diff(left) + if isinstance(right, bytes): + right = escape_for_readable_diff(right) 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 = [ - u"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 += [ - u"Skipping {} identical trailing " - u"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)) - explanation += [u"Strings contain only whitespace, escaping them using repr()"] - explanation += [ - line.strip("\n") - for line in ndiff(left.splitlines(keepends), right.splitlines(keepends)) - ] - return 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 = [ + u"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 += [ + u"Skipping {} identical trailing " + u"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)) + explanation += [u"Strings contain only whitespace, escaping them using repr()"] + explanation += [ + line.strip("\n") + for line in ndiff(left.splitlines(keepends), right.splitlines(keepends)) + ] + return explanation + + def _compare_eq_verbose(left, right): keepends = True left_lines = repr(left).splitlines(keepends) @@ -263,36 +263,36 @@ def _compare_eq_verbose(left, right): def _compare_eq_iterable(left, right, verbose=0): - if not verbose: - return [u"Use -v to get the full diff"] - # dynamic import to speedup pytest - import difflib - - try: - left_formatting = pprint.pformat(left).splitlines() - right_formatting = pprint.pformat(right).splitlines() - explanation = [u"Full diff:"] - except Exception: - # hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling - # sorted() on a list would raise. See issue #718. - # As a workaround, the full diff is generated by using the repr() string of each item of each container. - left_formatting = sorted(repr(x) for x in left) - right_formatting = sorted(repr(x) for x in right) - explanation = [u"Full diff (fallback to calling repr on each item):"] - explanation.extend( - line.strip() for line in difflib.ndiff(left_formatting, right_formatting) - ) - return explanation - - + if not verbose: + return [u"Use -v to get the full diff"] + # dynamic import to speedup pytest + import difflib + + try: + left_formatting = pprint.pformat(left).splitlines() + right_formatting = pprint.pformat(right).splitlines() + explanation = [u"Full diff:"] + except Exception: + # hack: PrettyPrinter.pformat() in python 2 fails when formatting items that can't be sorted(), ie, calling + # sorted() on a list would raise. See issue #718. + # As a workaround, the full diff is generated by using the repr() string of each item of each container. + left_formatting = sorted(repr(x) for x in left) + right_formatting = sorted(repr(x) for x in right) + explanation = [u"Full diff (fallback to calling repr on each item):"] + explanation.extend( + line.strip() for line in difflib.ndiff(left_formatting, right_formatting) + ) + return explanation + + def _compare_eq_sequence(left, right, verbose=0): - explanation = [] + explanation = [] len_left = len(left) len_right = len(right) for i in range(min(len_left, len_right)): - if left[i] != right[i]: - explanation += [u"At index %s diff: %r != %r" % (i, left[i], right[i])] - break + if left[i] != right[i]: + explanation += [u"At index %s diff: %r != %r" % (i, left[i], right[i])] + break len_diff = len_left - len_right if len_diff: @@ -311,39 +311,39 @@ def _compare_eq_sequence(left, right, verbose=0): u"%s contains %d more items, first extra item: %s" % (dir_with_more, len_diff, extra) ] - return explanation - - + return explanation + + def _compare_eq_set(left, right, verbose=0): - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append(u"Extra items in the left set:") - for item in diff_left: + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: + explanation.append(u"Extra items in the left set:") + for item in diff_left: explanation.append(saferepr(item)) - if diff_right: - explanation.append(u"Extra items in the right set:") - for item in diff_right: + if diff_right: + explanation.append(u"Extra items in the right set:") + for item in diff_right: explanation.append(saferepr(item)) - return explanation - - + return explanation + + def _compare_eq_dict(left, right, verbose=0): - explanation = [] + explanation = [] 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: - explanation += [u"Omitting %s identical items, use -vv to show" % len(same)] - elif same: - explanation += [u"Common items:"] - explanation += pprint.pformat(same).splitlines() - diff = {k for k in common if left[k] != right[k]} - if diff: - explanation += [u"Differing items:"] - for k in diff: + same = {k: left[k] for k in common if left[k] == right[k]} + if same and verbose < 2: + explanation += [u"Omitting %s identical items, use -vv to show" % len(same)] + elif same: + explanation += [u"Common items:"] + explanation += pprint.pformat(same).splitlines() + diff = {k for k in common if left[k] != right[k]} + if diff: + explanation += [u"Differing items:"] + 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) @@ -352,9 +352,9 @@ def _compare_eq_dict(left, right, verbose=0): u"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: @@ -362,12 +362,12 @@ def _compare_eq_dict(left, right, verbose=0): u"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, right, verbose, type_fns): isdatacls, isattrs = type_fns if isdatacls(left): @@ -403,19 +403,19 @@ def _compare_eq_cls(left, right, verbose, type_fns): def _notin_text(term, text, verbose=0): - index = text.find(term) - head = text[:index] - tail = text[index + len(term) :] - correct_text = head + tail - diff = _diff_text(correct_text, text, verbose) + index = text.find(term) + head = text[:index] + tail = text[index + len(term) :] + correct_text = head + tail + diff = _diff_text(correct_text, text, verbose) newdiff = [u"%s is contained here:" % saferepr(term, maxsize=42)] - for line in diff: - if line.startswith(u"Skipping"): - continue - if line.startswith(u"- "): - continue - if line.startswith(u"+ "): - newdiff.append(u" " + line[2:]) - else: - newdiff.append(line) - return newdiff + for line in diff: + if line.startswith(u"Skipping"): + continue + if line.startswith(u"- "): + continue + if line.startswith(u"+ "): + newdiff.append(u" " + line[2:]) + else: + newdiff.append(line) + return newdiff diff --git a/contrib/python/pytest/py2/_pytest/cacheprovider.py b/contrib/python/pytest/py2/_pytest/cacheprovider.py index 611653f8ba..f5c5545484 100644 --- a/contrib/python/pytest/py2/_pytest/cacheprovider.py +++ b/contrib/python/pytest/py2/_pytest/cacheprovider.py @@ -1,166 +1,166 @@ # -*- coding: utf-8 -*- -""" -merged implementation of the cache provider - -the name cache was not chosen to ensure pluggy automatically -ignores the external pytest-cache -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import json -import os -from collections import OrderedDict - -import attr -import py -import six - -import pytest -from .compat import _PY2 as PY2 -from .pathlib import Path -from .pathlib import resolve_from_str +""" +merged implementation of the cache provider + +the name cache was not chosen to ensure pluggy automatically +ignores the external pytest-cache +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import json +import os +from collections import OrderedDict + +import attr +import py +import six + +import pytest +from .compat import _PY2 as PY2 +from .pathlib import Path +from .pathlib import resolve_from_str from .pathlib import rm_rf - -README_CONTENT = u"""\ -# 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/latest/cache.html) for more information. -""" - + +README_CONTENT = u"""\ +# 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/latest/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 """ - - -@attr.s -class Cache(object): - _cachedir = attr.ib(repr=False) - _config = attr.ib(repr=False) - - @classmethod - def for_config(cls, config): - cachedir = cls.cache_dir_from_config(config) - if config.getoption("cacheclear") and cachedir.exists(): + + +@attr.s +class Cache(object): + _cachedir = attr.ib(repr=False) + _config = attr.ib(repr=False) + + @classmethod + def for_config(cls, config): + cachedir = cls.cache_dir_from_config(config) + if config.getoption("cacheclear") and cachedir.exists(): rm_rf(cachedir) - cachedir.mkdir() - return cls(cachedir, config) - - @staticmethod - def cache_dir_from_config(config): - return resolve_from_str(config.getini("cache_dir"), config.rootdir) - - def warn(self, fmt, **args): + cachedir.mkdir() + return cls(cachedir, config) + + @staticmethod + def cache_dir_from_config(config): + return resolve_from_str(config.getini("cache_dir"), config.rootdir) + + def warn(self, fmt, **args): from _pytest.warnings import _issue_warning_captured from _pytest.warning_types import PytestCacheWarning - + _issue_warning_captured( PytestCacheWarning(fmt.format(**args) if args else fmt), self._config.hook, - stacklevel=3, - ) - - def makedir(self, name): - """ 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 likes e. g. store/retrieve database - dumps across test sessions. - - :param name: must be a string not containing a ``/`` separator. - Make sure the name contains your plugin or application - identifiers to prevent clashes with other cache users. - """ - name = Path(name) - if len(name.parts) > 1: - raise ValueError("name is not allowed to contain path separators") - res = self._cachedir.joinpath("d", name) - res.mkdir(exist_ok=True, parents=True) - return py.path.local(res) - - def _getvaluepath(self, key): - return self._cachedir.joinpath("v", Path(key)) - - def get(self, key, default): - """ return cached value for the given key. If no value - was yet cached or the value cannot be read, the specified - 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: must be provided in case of a cache-miss or - invalid cache values. - - """ - path = self._getvaluepath(key) - try: - with path.open("r") as f: - return json.load(f) - except (ValueError, IOError, OSError): - return default - - def set(self, key, value): - """ 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 e. g. 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() + stacklevel=3, + ) + + def makedir(self, name): + """ 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 likes e. g. store/retrieve database + dumps across test sessions. + + :param name: must be a string not containing a ``/`` separator. + Make sure the name contains your plugin or application + identifiers to prevent clashes with other cache users. + """ + name = Path(name) + if len(name.parts) > 1: + raise ValueError("name is not allowed to contain path separators") + res = self._cachedir.joinpath("d", name) + res.mkdir(exist_ok=True, parents=True) + return py.path.local(res) + + def _getvaluepath(self, key): + return self._cachedir.joinpath("v", Path(key)) + + def get(self, key, default): + """ return cached value for the given key. If no value + was yet cached or the value cannot be read, the specified + 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: must be provided in case of a cache-miss or + invalid cache values. + + """ + path = self._getvaluepath(key) + try: + with path.open("r") as f: + return json.load(f) + except (ValueError, IOError, OSError): + return default + + def set(self, key, value): + """ 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 e. g. 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.parent.mkdir(exist_ok=True, parents=True) - except (IOError, OSError): - self.warn("could not create cache path {path}", path=path) - return + except (IOError, OSError): + self.warn("could not create cache path {path}", path=path) + return if not cache_dir_exists_already: self._ensure_supporting_files() - try: - f = path.open("wb" if PY2 else "w") - except (IOError, OSError): - self.warn("cache could not write path {path}", path=path) - else: - with f: - json.dump(value, f, indent=2, sort_keys=True) - - def _ensure_supporting_files(self): - """Create supporting files in the cache dir that are not really part of the cache.""" + try: + f = path.open("wb" if PY2 else "w") + except (IOError, OSError): + self.warn("cache could not write path {path}", path=path) + else: + with f: + json.dump(value, f, indent=2, sort_keys=True) + + def _ensure_supporting_files(self): + """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 = u"# Created by pytest automatically.\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 LFPlugin(object): - """ Plugin which implements the --lf (run last-failing) option """ - - def __init__(self, config): - self.config = config - active_keys = "lf", "failedfirst" - self.active = any(config.getoption(key) for key in active_keys) - self.lastfailed = config.cache.get("cache/lastfailed", {}) - self._previously_failed_count = None + + +class LFPlugin(object): + """ Plugin which implements the --lf (run last-failing) option """ + + def __init__(self, config): + self.config = config + active_keys = "lf", "failedfirst" + self.active = any(config.getoption(key) for key in active_keys) + self.lastfailed = config.cache.get("cache/lastfailed", {}) + self._previously_failed_count = None self._report_status = None self._skipped_files = 0 # count skipped files during collection due to --lf - + def last_failed_paths(self): """Returns a set with all Paths()s of the previously failed nodeids (cached). """ @@ -186,26 +186,26 @@ class LFPlugin(object): self._skipped_files += 1 return skip_it - def pytest_report_collectionfinish(self): - if self.active and self.config.getoption("verbose") >= 0: + def pytest_report_collectionfinish(self): + if self.active and self.config.getoption("verbose") >= 0: return "run-last-failure: %s" % self._report_status - - def pytest_runtest_logreport(self, report): - 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): - 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 - - def pytest_collection_modifyitems(self, session, config, items): + + def pytest_runtest_logreport(self, report): + 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): + 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 + + def pytest_collection_modifyitems(self, session, config, items): if not self.active: return @@ -226,11 +226,11 @@ class LFPlugin(object): 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 "" @@ -247,192 +247,192 @@ class LFPlugin(object): self._report_status = "no previously failed tests, " if self.config.getoption("last_failed_no_failures") == "none": self._report_status += "deselecting all items." - config.hook.pytest_deselected(items=items) - items[:] = [] + config.hook.pytest_deselected(items=items) + items[:] = [] else: self._report_status += "not deselecting items." - - def pytest_sessionfinish(self, session): - config = self.config - if config.getoption("cacheshow") or hasattr(config, "slaveinput"): - return - - saved_lastfailed = config.cache.get("cache/lastfailed", {}) - if saved_lastfailed != self.lastfailed: - config.cache.set("cache/lastfailed", self.lastfailed) - - -class NFPlugin(object): - """ Plugin which implements the --nf (run new-first) option """ - - def __init__(self, config): - self.config = config - self.active = config.option.newfirst - self.cached_nodeids = config.cache.get("cache/nodeids", []) - - def pytest_collection_modifyitems(self, session, config, items): - if self.active: - new_items = OrderedDict() - other_items = OrderedDict() - 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( - six.itervalues(new_items) - ) + self._get_increasing_order(six.itervalues(other_items)) - self.cached_nodeids = [x.nodeid for x in items if isinstance(x, pytest.Item)] - - def _get_increasing_order(self, items): - return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) - - def pytest_sessionfinish(self, session): - config = self.config - if config.getoption("cacheshow") or hasattr(config, "slaveinput"): - return - - config.cache.set("cache/nodeids", self.cached_nodeids) - - -def pytest_addoption(parser): - 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. " - "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", + + def pytest_sessionfinish(self, session): + config = self.config + if config.getoption("cacheshow") or hasattr(config, "slaveinput"): + return + + saved_lastfailed = config.cache.get("cache/lastfailed", {}) + if saved_lastfailed != self.lastfailed: + config.cache.set("cache/lastfailed", self.lastfailed) + + +class NFPlugin(object): + """ Plugin which implements the --nf (run new-first) option """ + + def __init__(self, config): + self.config = config + self.active = config.option.newfirst + self.cached_nodeids = config.cache.get("cache/nodeids", []) + + def pytest_collection_modifyitems(self, session, config, items): + if self.active: + new_items = OrderedDict() + other_items = OrderedDict() + 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( + six.itervalues(new_items) + ) + self._get_increasing_order(six.itervalues(other_items)) + self.cached_nodeids = [x.nodeid for x in items if isinstance(x, pytest.Item)] + + def _get_increasing_order(self, items): + return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True) + + def pytest_sessionfinish(self, session): + config = self.config + if config.getoption("cacheshow") or hasattr(config, "slaveinput"): + return + + config.cache.set("cache/nodeids", self.cached_nodeids) + + +def pytest_addoption(parser): + 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. " + "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", 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): - if config.option.cacheshow: - from _pytest.main import wrap_session - - return wrap_session(config, cacheshow) - - -@pytest.hookimpl(tryfirst=True) -def pytest_configure(config): - config.cache = Cache.for_config(config) - config.pluginmanager.register(LFPlugin(config), "lfplugin") - config.pluginmanager.register(NFPlugin(config), "nfplugin") - - -@pytest.fixture -def cache(request): - """ - Return a cache object that can persist state between testing sessions. - - cache.get(key, default) - cache.set(key, value) - - Keys must be a ``/`` separated value, 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. - """ - return request.config.cache - - -def pytest_report_header(config): - """Display cachedir with --cache-show and if non-default.""" + ) + + +def pytest_cmdline_main(config): + if config.option.cacheshow: + from _pytest.main import wrap_session + + return wrap_session(config, cacheshow) + + +@pytest.hookimpl(tryfirst=True) +def pytest_configure(config): + config.cache = Cache.for_config(config) + config.pluginmanager.register(LFPlugin(config), "lfplugin") + config.pluginmanager.register(NFPlugin(config), "nfplugin") + + +@pytest.fixture +def cache(request): + """ + Return a cache object that can persist state between testing sessions. + + cache.get(key, default) + cache.set(key, value) + + Keys must be a ``/`` separated value, 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. + """ + return request.config.cache + + +def pytest_report_header(config): + """Display cachedir with --cache-show and if non-default.""" if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache": - cachedir = config.cache._cachedir - # TODO: evaluate generating upward relative paths - # starting with .., ../.. if sensible - - try: - displaypath = cachedir.relative_to(config.rootdir) - except ValueError: - displaypath = cachedir - return "cachedir: {}".format(displaypath) - - -def cacheshow(config, session): - from pprint import pformat - - tw = py.io.TerminalWriter() - tw.line("cachedir: " + str(config.cache._cachedir)) - if not config.cache._cachedir.is_dir(): - tw.line("cache is empty") - return 0 + cachedir = config.cache._cachedir + # TODO: evaluate generating upward relative paths + # starting with .., ../.. if sensible + + try: + displaypath = cachedir.relative_to(config.rootdir) + except ValueError: + displaypath = cachedir + return "cachedir: {}".format(displaypath) + + +def cacheshow(config, session): + from pprint import pformat + + tw = py.io.TerminalWriter() + 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 - vdir = basedir / "v" + dummy = object() + basedir = config.cache._cachedir + vdir = basedir / "v" tw.sep("-", "cache values for %r" % glob) for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()): - key = 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) - - ddir = basedir / "d" - if ddir.is_dir(): + key = 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) + + ddir = basedir / "d" + 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(): - key = p.relative_to(basedir) - tw.line("{} is a file of length {:d}".format(key, p.stat().st_size)) - return 0 + for p in contents: + # if p.check(dir=1): + # print("%s/" % p.relto(basedir)) + if p.is_file(): + key = p.relative_to(basedir) + tw.line("{} is a file of length {:d}".format(key, p.stat().st_size)) + return 0 diff --git a/contrib/python/pytest/py2/_pytest/capture.py b/contrib/python/pytest/py2/_pytest/capture.py index 676c1d9eb5..68c17772f3 100644 --- a/contrib/python/pytest/py2/_pytest/capture.py +++ b/contrib/python/pytest/py2/_pytest/capture.py @@ -1,90 +1,90 @@ # -*- coding: utf-8 -*- -""" -per-test stdout/stderr capturing mechanism. - -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import collections -import contextlib -import io -import os -import sys -from io import UnsupportedOperation -from tempfile import TemporaryFile - -import six - -import pytest +""" +per-test stdout/stderr capturing mechanism. + +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import contextlib +import io +import os +import sys +from io import UnsupportedOperation +from tempfile import TemporaryFile + +import six + +import pytest from _pytest.compat import _PY3 -from _pytest.compat import CaptureIO - -patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} - - -def pytest_addoption(parser): - group = parser.getgroup("general") - group._addoption( - "--capture", - action="store", - default="fd" if hasattr(os, "dup") else "sys", - metavar="method", - choices=["fd", "sys", "no"], - help="per-test capturing method: one of fd|sys|no.", - ) - group._addoption( - "-s", - action="store_const", - const="no", - dest="capture", - help="shortcut for --capture=no.", - ) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_load_initial_conftests(early_config, parser, args): - 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) - - # finally trigger conftest loading but while capturing (issue93) - 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) - - -class CaptureManager(object): - """ - 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: which is 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): - self._method = method - self._global_capturing = None - self._current_item = None - +from _pytest.compat import CaptureIO + +patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} + + +def pytest_addoption(parser): + group = parser.getgroup("general") + group._addoption( + "--capture", + action="store", + default="fd" if hasattr(os, "dup") else "sys", + metavar="method", + choices=["fd", "sys", "no"], + help="per-test capturing method: one of fd|sys|no.", + ) + group._addoption( + "-s", + action="store_const", + const="no", + dest="capture", + help="shortcut for --capture=no.", + ) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_load_initial_conftests(early_config, parser, args): + 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) + + # finally trigger conftest loading but while capturing (issue93) + 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) + + +class CaptureManager(object): + """ + 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: which is 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): + self._method = method + self._global_capturing = None + self._current_item = None + def __repr__(self): return "<CaptureManager _method=%r _global_capturing=%r _current_item=%r>" % ( self._method, @@ -92,15 +92,15 @@ class CaptureManager(object): self._current_item, ) - def _getcapture(self, method): - if method == "fd": - return MultiCapture(out=True, err=True, Capture=FDCapture) - elif method == "sys": - return MultiCapture(out=True, err=True, Capture=SysCapture) - elif method == "no": - return MultiCapture(out=False, err=False, in_=False) + def _getcapture(self, method): + if method == "fd": + return MultiCapture(out=True, err=True, Capture=FDCapture) + elif method == "sys": + return MultiCapture(out=True, err=True, Capture=SysCapture) + elif method == "no": + return MultiCapture(out=False, err=False, in_=False) raise ValueError("unknown capturing method: %r" % method) # pragma: no cover - + def is_capturing(self): if self.is_globally_capturing(): return "global" @@ -111,33 +111,33 @@ class CaptureManager(object): ) return False - # Global capturing control - - def is_globally_capturing(self): - return self._method != "no" - - def start_global_capturing(self): - assert self._global_capturing is None - self._global_capturing = self._getcapture(self._method) - self._global_capturing.start_capturing() - - def stop_global_capturing(self): - 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): + # Global capturing control + + def is_globally_capturing(self): + return self._method != "no" + + def start_global_capturing(self): + assert self._global_capturing is None + self._global_capturing = self._getcapture(self._method) + self._global_capturing.start_capturing() + + def stop_global_capturing(self): + 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): # 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_=False): - cap = getattr(self, "_global_capturing", None) - if cap is not None: - cap.suspend_capturing(in_=in_) - + + def suspend_global_capture(self, in_=False): + cap = getattr(self, "_global_capturing", None) + if cap is not None: + cap.suspend_capturing(in_=in_) + def suspend(self, in_=False): # Need to undo local capsys-et-al if it exists before disabling global capture. self.suspend_fixture(self._current_item) @@ -147,331 +147,331 @@ class CaptureManager(object): self.resume_global_capture() self.resume_fixture(self._current_item) - def read_global_capture(self): - return self._global_capturing.readouterr() - - # Fixture Control (it's just forwarding, think about removing this later) - - def activate_fixture(self, item): - """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over - the global capture. - """ - fixture = getattr(item, "_capture_fixture", None) - if fixture is not None: - fixture._start() - - def deactivate_fixture(self, item): - """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" - fixture = getattr(item, "_capture_fixture", None) - if fixture is not None: - fixture.close() - - def suspend_fixture(self, item): - fixture = getattr(item, "_capture_fixture", None) - if fixture is not None: - fixture._suspend() - - def resume_fixture(self, item): - fixture = getattr(item, "_capture_fixture", None) - if fixture is not None: - fixture._resume() - - # Helper context managers - - @contextlib.contextmanager - def global_and_fixture_disabled(self): + def read_global_capture(self): + return self._global_capturing.readouterr() + + # Fixture Control (it's just forwarding, think about removing this later) + + def activate_fixture(self, item): + """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over + the global capture. + """ + fixture = getattr(item, "_capture_fixture", None) + if fixture is not None: + fixture._start() + + def deactivate_fixture(self, item): + """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any.""" + fixture = getattr(item, "_capture_fixture", None) + if fixture is not None: + fixture.close() + + def suspend_fixture(self, item): + fixture = getattr(item, "_capture_fixture", None) + if fixture is not None: + fixture._suspend() + + def resume_fixture(self, item): + fixture = getattr(item, "_capture_fixture", None) + if fixture is not None: + fixture._resume() + + # Helper context managers + + @contextlib.contextmanager + def global_and_fixture_disabled(self): """Context manager to temporarily disable global and current fixture capturing.""" self.suspend() - try: - yield - finally: + try: + yield + finally: self.resume() - - @contextlib.contextmanager - def item_capture(self, when, item): - self.resume_global_capture() - self.activate_fixture(item) - try: - yield - finally: - self.deactivate_fixture(item) - 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 - - @pytest.hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector): - if isinstance(collector, pytest.File): - self.resume_global_capture() - outcome = yield - self.suspend_global_capture() - out, err = self.read_global_capture() - rep = outcome.get_result() - if out: - rep.sections.append(("Captured stdout", out)) - if err: - rep.sections.append(("Captured stderr", err)) - else: - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_protocol(self, item): - self._current_item = item - yield - self._current_item = None - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_setup(self, item): - with self.item_capture("setup", item): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item): - with self.item_capture("call", item): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_teardown(self, item): - with self.item_capture("teardown", item): - yield - - @pytest.hookimpl(tryfirst=True) - def pytest_keyboard_interrupt(self, excinfo): - self.stop_global_capturing() - - @pytest.hookimpl(tryfirst=True) - def pytest_internalerror(self, excinfo): - self.stop_global_capturing() - - -capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"} - - -def _ensure_only_one_capture_fixture(request, name): - fixtures = set(request.fixturenames) & capture_fixtures - {name} - if fixtures: - fixtures = sorted(fixtures) - fixtures = fixtures[0] if len(fixtures) == 1 else fixtures - raise request.raiseerror( - "cannot use {} and {} at the same time".format(fixtures, name) - ) - - -@pytest.fixture -def capsys(request): + + @contextlib.contextmanager + def item_capture(self, when, item): + self.resume_global_capture() + self.activate_fixture(item) + try: + yield + finally: + self.deactivate_fixture(item) + 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 + + @pytest.hookimpl(hookwrapper=True) + def pytest_make_collect_report(self, collector): + if isinstance(collector, pytest.File): + self.resume_global_capture() + outcome = yield + self.suspend_global_capture() + out, err = self.read_global_capture() + rep = outcome.get_result() + if out: + rep.sections.append(("Captured stdout", out)) + if err: + rep.sections.append(("Captured stderr", err)) + else: + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_protocol(self, item): + self._current_item = item + yield + self._current_item = None + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_setup(self, item): + with self.item_capture("setup", item): + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_call(self, item): + with self.item_capture("call", item): + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_teardown(self, item): + with self.item_capture("teardown", item): + yield + + @pytest.hookimpl(tryfirst=True) + def pytest_keyboard_interrupt(self, excinfo): + self.stop_global_capturing() + + @pytest.hookimpl(tryfirst=True) + def pytest_internalerror(self, excinfo): + self.stop_global_capturing() + + +capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"} + + +def _ensure_only_one_capture_fixture(request, name): + fixtures = set(request.fixturenames) & capture_fixtures - {name} + if fixtures: + fixtures = sorted(fixtures) + fixtures = fixtures[0] if len(fixtures) == 1 else fixtures + raise request.raiseerror( + "cannot use {} and {} at the same time".format(fixtures, name) + ) + + +@pytest.fixture +def capsys(request): """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. - """ - _ensure_only_one_capture_fixture(request, "capsys") - with _install_capture_fixture_on_item(request, SysCapture) as fixture: - yield fixture - - -@pytest.fixture -def capsysbinary(request): + """ + _ensure_only_one_capture_fixture(request, "capsys") + with _install_capture_fixture_on_item(request, SysCapture) as fixture: + yield fixture + + +@pytest.fixture +def capsysbinary(request): """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``bytes`` objects. - """ - _ensure_only_one_capture_fixture(request, "capsysbinary") - # Currently, the implementation uses the python3 specific `.buffer` - # property of CaptureIO. - if sys.version_info < (3,): + """ + _ensure_only_one_capture_fixture(request, "capsysbinary") + # Currently, the implementation uses the python3 specific `.buffer` + # property of CaptureIO. + if sys.version_info < (3,): raise request.raiseerror("capsysbinary is only supported on Python 3") - with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture: - yield fixture - - -@pytest.fixture -def capfd(request): + with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture: + yield fixture + + +@pytest.fixture +def capfd(request): """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. - """ - _ensure_only_one_capture_fixture(request, "capfd") - if not hasattr(os, "dup"): - pytest.skip( - "capfd fixture needs os.dup function which is not available in this system" - ) - with _install_capture_fixture_on_item(request, FDCapture) as fixture: - yield fixture - - -@pytest.fixture -def capfdbinary(request): + """ + _ensure_only_one_capture_fixture(request, "capfd") + if not hasattr(os, "dup"): + pytest.skip( + "capfd fixture needs os.dup function which is not available in this system" + ) + with _install_capture_fixture_on_item(request, FDCapture) as fixture: + yield fixture + + +@pytest.fixture +def capfdbinary(request): """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. - """ - _ensure_only_one_capture_fixture(request, "capfdbinary") - if not hasattr(os, "dup"): - pytest.skip( - "capfdbinary fixture needs os.dup function which is not available in this system" - ) - with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture: - yield fixture - - -@contextlib.contextmanager -def _install_capture_fixture_on_item(request, capture_class): - """ - Context manager which creates a ``CaptureFixture`` instance and "installs" it on - the item/node of the given request. Used by ``capsys`` and ``capfd``. - - The CaptureFixture is added as attribute of the item because it needs to accessed - by ``CaptureManager`` during its ``pytest_runtest_*`` hooks. - """ - request.node._capture_fixture = fixture = CaptureFixture(capture_class, request) - capmanager = request.config.pluginmanager.getplugin("capturemanager") + """ + _ensure_only_one_capture_fixture(request, "capfdbinary") + if not hasattr(os, "dup"): + pytest.skip( + "capfdbinary fixture needs os.dup function which is not available in this system" + ) + with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture: + yield fixture + + +@contextlib.contextmanager +def _install_capture_fixture_on_item(request, capture_class): + """ + Context manager which creates a ``CaptureFixture`` instance and "installs" it on + the item/node of the given request. Used by ``capsys`` and ``capfd``. + + The CaptureFixture is added as attribute of the item because it needs to accessed + by ``CaptureManager`` during its ``pytest_runtest_*`` hooks. + """ + request.node._capture_fixture = fixture = CaptureFixture(capture_class, request) + capmanager = request.config.pluginmanager.getplugin("capturemanager") # Need to active this fixture right away in case it is being used by another fixture (setup phase). # If this fixture is being used only by a test function (call phase), then we wouldn't need this # activation, but it doesn't hurt. - capmanager.activate_fixture(request.node) - yield fixture - fixture.close() - del request.node._capture_fixture - - -class CaptureFixture(object): - """ - Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary` - fixtures. - """ - - def __init__(self, captureclass, request): - self.captureclass = captureclass - self.request = request - self._capture = None - self._captured_out = self.captureclass.EMPTY_BUFFER - self._captured_err = self.captureclass.EMPTY_BUFFER - - def _start(self): + capmanager.activate_fixture(request.node) + yield fixture + fixture.close() + del request.node._capture_fixture + + +class CaptureFixture(object): + """ + Object returned by :py:func:`capsys`, :py:func:`capsysbinary`, :py:func:`capfd` and :py:func:`capfdbinary` + fixtures. + """ + + def __init__(self, captureclass, request): + self.captureclass = captureclass + self.request = request + self._capture = None + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER + + def _start(self): if self._capture is None: - self._capture = MultiCapture( - out=True, err=True, in_=False, Capture=self.captureclass - ) - self._capture.start_capturing() - - def close(self): - if self._capture is not None: - out, err = self._capture.pop_outerr_to_orig() - self._captured_out += out - self._captured_err += err - self._capture.stop_capturing() - self._capture = None - - def readouterr(self): - """Read and return the captured output so far, resetting the internal buffer. - + self._capture = MultiCapture( + out=True, err=True, in_=False, Capture=self.captureclass + ) + self._capture.start_capturing() + + def close(self): + if self._capture is not None: + out, err = self._capture.pop_outerr_to_orig() + self._captured_out += out + self._captured_err += err + self._capture.stop_capturing() + self._capture = None + + def readouterr(self): + """Read and return the captured output so far, resetting the internal buffer. + :return: captured content as a namedtuple with ``out`` and ``err`` string attributes - """ - captured_out, captured_err = self._captured_out, self._captured_err - if self._capture is not None: - out, err = self._capture.readouterr() - captured_out += out - captured_err += err - self._captured_out = self.captureclass.EMPTY_BUFFER - self._captured_err = self.captureclass.EMPTY_BUFFER - return CaptureResult(captured_out, captured_err) - - def _suspend(self): - """Suspends this fixture's own capturing temporarily.""" + """ + captured_out, captured_err = self._captured_out, self._captured_err + if self._capture is not None: + out, err = self._capture.readouterr() + captured_out += out + captured_err += err + self._captured_out = self.captureclass.EMPTY_BUFFER + self._captured_err = self.captureclass.EMPTY_BUFFER + return CaptureResult(captured_out, captured_err) + + def _suspend(self): + """Suspends this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.suspend_capturing() - - def _resume(self): - """Resumes this fixture's own capturing temporarily.""" + + def _resume(self): + """Resumes this fixture's own capturing temporarily.""" if self._capture is not None: self._capture.resume_capturing() - - @contextlib.contextmanager - def disabled(self): - """Temporarily disables capture while inside the 'with' block.""" - capmanager = self.request.config.pluginmanager.getplugin("capturemanager") - with capmanager.global_and_fixture_disabled(): - yield - - -def safe_text_dupfile(f, mode, default_encoding="UTF8"): - """ return an open text file object that's a duplicate of f on the - FD-level if possible. - """ - encoding = getattr(f, "encoding", None) - try: - fd = f.fileno() - except Exception: - if "b" not in getattr(f, "mode", "") and hasattr(f, "encoding"): - # we seem to have a text stream, let's just use it - return f - else: - newfd = os.dup(fd) - if "b" not in mode: - mode += "b" - f = os.fdopen(newfd, mode, 0) # no buffering - return EncodedFile(f, encoding or default_encoding) - - -class EncodedFile(object): - errors = "strict" # possibly needed by py3 code (issue555) - - def __init__(self, buffer, encoding): - self.buffer = buffer - self.encoding = encoding - - def write(self, obj): - if isinstance(obj, six.text_type): - obj = obj.encode(self.encoding, "replace") + + @contextlib.contextmanager + def disabled(self): + """Temporarily disables capture while inside the 'with' block.""" + capmanager = self.request.config.pluginmanager.getplugin("capturemanager") + with capmanager.global_and_fixture_disabled(): + yield + + +def safe_text_dupfile(f, mode, default_encoding="UTF8"): + """ return an open text file object that's a duplicate of f on the + FD-level if possible. + """ + encoding = getattr(f, "encoding", None) + try: + fd = f.fileno() + except Exception: + if "b" not in getattr(f, "mode", "") and hasattr(f, "encoding"): + # we seem to have a text stream, let's just use it + return f + else: + newfd = os.dup(fd) + if "b" not in mode: + mode += "b" + f = os.fdopen(newfd, mode, 0) # no buffering + return EncodedFile(f, encoding or default_encoding) + + +class EncodedFile(object): + errors = "strict" # possibly needed by py3 code (issue555) + + def __init__(self, buffer, encoding): + self.buffer = buffer + self.encoding = encoding + + def write(self, obj): + if isinstance(obj, six.text_type): + obj = obj.encode(self.encoding, "replace") elif _PY3: raise TypeError( "write() argument must be str, not {}".format(type(obj).__name__) ) - self.buffer.write(obj) - - def writelines(self, linelist): - data = "".join(linelist) - self.write(data) - - @property - def name(self): - """Ensure that file.name is a string.""" - return repr(self.buffer) - + self.buffer.write(obj) + + def writelines(self, linelist): + data = "".join(linelist) + self.write(data) + + @property + def name(self): + """Ensure that file.name is a string.""" + return repr(self.buffer) + @property def mode(self): return self.buffer.mode.replace("b", "") - def __getattr__(self, name): - return getattr(object.__getattribute__(self, "buffer"), name) - - -CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) - - -class MultiCapture(object): - out = err = in_ = None + def __getattr__(self, name): + return getattr(object.__getattribute__(self, "buffer"), name) + + +CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) + + +class MultiCapture(object): + out = err = in_ = None _state = None - - def __init__(self, out=True, err=True, in_=True, Capture=None): - if in_: - self.in_ = Capture(0) - if out: - self.out = Capture(1) - if err: - self.err = Capture(2) - + + def __init__(self, out=True, err=True, in_=True, Capture=None): + if in_: + self.in_ = Capture(0) + if out: + self.out = Capture(1) + if err: + self.err = Capture(2) + def __repr__(self): return "<MultiCapture out=%r err=%r in_=%r _state=%r _in_suspended=%r>" % ( self.out, @@ -481,185 +481,185 @@ class MultiCapture(object): getattr(self, "_in_suspended", "<UNSET>"), ) - def start_capturing(self): + def start_capturing(self): self._state = "started" - if self.in_: - self.in_.start() - if self.out: - self.out.start() - if self.err: - self.err.start() - - def pop_outerr_to_orig(self): - """ 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 - - def suspend_capturing(self, in_=False): + if self.in_: + self.in_.start() + if self.out: + self.out.start() + if self.err: + self.err.start() + + def pop_outerr_to_orig(self): + """ 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 + + def suspend_capturing(self, in_=False): 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 - - def resume_capturing(self): + 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): self._state = "resumed" - if self.out: - self.out.resume() - if self.err: - self.err.resume() - if hasattr(self, "_in_suspended"): - self.in_.resume() - del self._in_suspended - - def stop_capturing(self): - """ stop capturing and reset capturing streams """ + if self.out: + self.out.resume() + if self.err: + self.err.resume() + if hasattr(self, "_in_suspended"): + self.in_.resume() + del self._in_suspended + + def stop_capturing(self): + """ 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() - - def readouterr(self): - """ return snapshot unicode value of stdout/stderr capturings. """ - return CaptureResult( - self.out.snap() if self.out is not None else "", - self.err.snap() if self.err is not None else "", - ) - - -class NoCapture(object): - EMPTY_BUFFER = None - __init__ = start = done = suspend = resume = lambda *args: None - - -class FDCaptureBinary(object): - """Capture IO to/from a given os-level filedescriptor. - - snap() produces `bytes` - """ - - EMPTY_BUFFER = b"" + if self.out: + self.out.done() + if self.err: + self.err.done() + if self.in_: + self.in_.done() + + def readouterr(self): + """ return snapshot unicode value of stdout/stderr capturings. """ + return CaptureResult( + self.out.snap() if self.out is not None else "", + self.err.snap() if self.err is not None else "", + ) + + +class NoCapture(object): + EMPTY_BUFFER = None + __init__ = start = done = suspend = resume = lambda *args: None + + +class FDCaptureBinary(object): + """Capture IO to/from a given os-level filedescriptor. + + snap() produces `bytes` + """ + + EMPTY_BUFFER = b"" _state = None - - def __init__(self, targetfd, tmpfile=None): - self.targetfd = targetfd - try: - self.targetfd_save = os.dup(self.targetfd) - except OSError: - self.start = lambda: None - self.done = lambda: None - else: - if targetfd == 0: - assert not tmpfile, "cannot set tmpfile with stdin" - tmpfile = open(os.devnull, "r") - self.syscapture = SysCapture(targetfd) - else: - if tmpfile is None: - f = TemporaryFile() - with f: - tmpfile = safe_text_dupfile(f, mode="wb+") - if targetfd in patchsysdict: - self.syscapture = SysCapture(targetfd, tmpfile) - else: - self.syscapture = NoCapture() - self.tmpfile = tmpfile - self.tmpfile_fd = tmpfile.fileno() - - def __repr__(self): + + def __init__(self, targetfd, tmpfile=None): + self.targetfd = targetfd + try: + self.targetfd_save = os.dup(self.targetfd) + except OSError: + self.start = lambda: None + self.done = lambda: None + else: + if targetfd == 0: + assert not tmpfile, "cannot set tmpfile with stdin" + tmpfile = open(os.devnull, "r") + self.syscapture = SysCapture(targetfd) + else: + if tmpfile is None: + f = TemporaryFile() + with f: + tmpfile = safe_text_dupfile(f, mode="wb+") + if targetfd in patchsysdict: + self.syscapture = SysCapture(targetfd, tmpfile) + else: + self.syscapture = NoCapture() + self.tmpfile = tmpfile + self.tmpfile_fd = tmpfile.fileno() + + def __repr__(self): return "<FDCapture %s oldfd=%s _state=%r>" % ( self.targetfd, getattr(self, "targetfd_save", None), self._state, ) - - def start(self): - """ Start capturing on targetfd using memorized tmpfile. """ - try: - os.fstat(self.targetfd_save) - except (AttributeError, OSError): - raise ValueError("saved filedescriptor not valid anymore") - os.dup2(self.tmpfile_fd, self.targetfd) - self.syscapture.start() + + def start(self): + """ Start capturing on targetfd using memorized tmpfile. """ + try: + os.fstat(self.targetfd_save) + except (AttributeError, OSError): + raise ValueError("saved filedescriptor not valid anymore") + os.dup2(self.tmpfile_fd, self.targetfd) + self.syscapture.start() self._state = "started" - - def snap(self): - self.tmpfile.seek(0) - res = self.tmpfile.read() - self.tmpfile.seek(0) - self.tmpfile.truncate() - return res - - def done(self): - """ stop capturing, restore streams, return original capture file, - seeked to position zero. """ - targetfd_save = self.__dict__.pop("targetfd_save") - os.dup2(targetfd_save, self.targetfd) - os.close(targetfd_save) - self.syscapture.done() - _attempt_to_close_capture_file(self.tmpfile) + + def snap(self): + self.tmpfile.seek(0) + res = self.tmpfile.read() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def done(self): + """ stop capturing, restore streams, return original capture file, + seeked to position zero. """ + targetfd_save = self.__dict__.pop("targetfd_save") + os.dup2(targetfd_save, self.targetfd) + os.close(targetfd_save) + self.syscapture.done() + _attempt_to_close_capture_file(self.tmpfile) self._state = "done" - - def suspend(self): - self.syscapture.suspend() - os.dup2(self.targetfd_save, self.targetfd) + + def suspend(self): + self.syscapture.suspend() + os.dup2(self.targetfd_save, self.targetfd) self._state = "suspended" - - def resume(self): - self.syscapture.resume() - os.dup2(self.tmpfile_fd, self.targetfd) + + def resume(self): + self.syscapture.resume() + os.dup2(self.tmpfile_fd, self.targetfd) self._state = "resumed" - - def writeorg(self, data): - """ write to original file descriptor. """ - if isinstance(data, six.text_type): - data = data.encode("utf8") # XXX use encoding of original stream - os.write(self.targetfd_save, data) - - -class FDCapture(FDCaptureBinary): - """Capture IO to/from a given os-level filedescriptor. - - snap() produces text - """ - - EMPTY_BUFFER = str() - - def snap(self): + + def writeorg(self, data): + """ write to original file descriptor. """ + if isinstance(data, six.text_type): + data = data.encode("utf8") # XXX use encoding of original stream + os.write(self.targetfd_save, data) + + +class FDCapture(FDCaptureBinary): + """Capture IO to/from a given os-level filedescriptor. + + snap() produces text + """ + + EMPTY_BUFFER = str() + + def snap(self): res = super(FDCapture, self).snap() - enc = getattr(self.tmpfile, "encoding", None) - if enc and isinstance(res, bytes): - res = six.text_type(res, enc, "replace") - return res - - -class SysCapture(object): - - EMPTY_BUFFER = str() + enc = getattr(self.tmpfile, "encoding", None) + if enc and isinstance(res, bytes): + res = six.text_type(res, enc, "replace") + return res + + +class SysCapture(object): + + EMPTY_BUFFER = str() _state = None - - def __init__(self, fd, tmpfile=None): - name = patchsysdict[fd] - self._old = getattr(sys, name) - self.name = name - if tmpfile is None: - if name == "stdin": - tmpfile = DontReadFromInput() - else: - tmpfile = CaptureIO() - self.tmpfile = tmpfile - + + def __init__(self, fd, tmpfile=None): + name = patchsysdict[fd] + self._old = getattr(sys, name) + self.name = name + if tmpfile is None: + if name == "stdin": + tmpfile = DontReadFromInput() + else: + tmpfile = CaptureIO() + self.tmpfile = tmpfile + def __repr__(self): return "<SysCapture %s _old=%r, tmpfile=%r _state=%r>" % ( self.name, @@ -668,183 +668,183 @@ class SysCapture(object): self._state, ) - def start(self): - setattr(sys, self.name, self.tmpfile) + def start(self): + setattr(sys, self.name, self.tmpfile) self._state = "started" - - def snap(self): - res = self.tmpfile.getvalue() - self.tmpfile.seek(0) - self.tmpfile.truncate() - return res - - def done(self): - setattr(sys, self.name, self._old) - del self._old - _attempt_to_close_capture_file(self.tmpfile) + + def snap(self): + res = self.tmpfile.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + def done(self): + setattr(sys, self.name, self._old) + del self._old + _attempt_to_close_capture_file(self.tmpfile) self._state = "done" - - def suspend(self): - setattr(sys, self.name, self._old) + + def suspend(self): + setattr(sys, self.name, self._old) self._state = "suspended" - - def resume(self): - setattr(sys, self.name, self.tmpfile) + + def resume(self): + setattr(sys, self.name, self.tmpfile) self._state = "resumed" - - def writeorg(self, data): - self._old.write(data) - self._old.flush() - - -class SysCaptureBinary(SysCapture): - EMPTY_BUFFER = b"" - - def snap(self): - res = self.tmpfile.buffer.getvalue() - self.tmpfile.seek(0) - self.tmpfile.truncate() - return res - - -class DontReadFromInput(six.Iterator): - """Temporary stub class. Ideally when stdin is accessed, the - capturing should be turned off, with possibly all data captured - so far sent to the screen. This should be configurable, though, - because in automated test runs it is better to crash than - hang indefinitely. - """ - - encoding = None - - def read(self, *args): - raise IOError("reading from stdin while output is captured") - - readline = read - readlines = read - __next__ = read - - def __iter__(self): - return self - - def fileno(self): - raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") - - def isatty(self): - return False - - def close(self): - pass - - @property - def buffer(self): - if sys.version_info >= (3, 0): - return self - else: - raise AttributeError("redirected stdin has no attribute buffer") - - -def _colorama_workaround(): - """ - Ensure colorama is imported so that it attaches to the correct stdio - handles on Windows. - - colorama uses the terminal on import time. So if something does the - first import of colorama while I/O capture is active, colorama will - fail in various ways. - """ + + def writeorg(self, data): + self._old.write(data) + self._old.flush() + + +class SysCaptureBinary(SysCapture): + EMPTY_BUFFER = b"" + + def snap(self): + res = self.tmpfile.buffer.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + +class DontReadFromInput(six.Iterator): + """Temporary stub class. Ideally when stdin is accessed, the + capturing should be turned off, with possibly all data captured + so far sent to the screen. This should be configurable, though, + because in automated test runs it is better to crash than + hang indefinitely. + """ + + encoding = None + + def read(self, *args): + raise IOError("reading from stdin while output is captured") + + readline = read + readlines = read + __next__ = read + + def __iter__(self): + return self + + def fileno(self): + raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()") + + def isatty(self): + return False + + def close(self): + pass + + @property + def buffer(self): + if sys.version_info >= (3, 0): + return self + else: + raise AttributeError("redirected stdin has no attribute buffer") + + +def _colorama_workaround(): + """ + Ensure colorama is imported so that it attaches to the correct stdio + handles on Windows. + + colorama uses the terminal on import time. So if something does the + first import of colorama while I/O capture is active, colorama will + fail in various ways. + """ if sys.platform.startswith("win32"): try: import colorama # noqa: F401 except ImportError: pass - - -def _readline_workaround(): - """ - Ensure readline is imported so that it attaches to the correct stdio - handles on Windows. - - Pdb uses readline support where available--when not running from the Python - prompt, the readline module is not imported until running the pdb REPL. If - running pytest with the --pdb option this means the readline module is not - imported until after I/O capture has been started. - - This is a problem for pyreadline, which is often used to implement readline - support on Windows, as it does not attach to the correct handles for stdout - and/or stdin if they have been redirected by the FDCapture mechanism. This - workaround ensures that readline is imported before I/O capture is setup so - that it can attach to the actual stdin/out for the console. - - See https://github.com/pytest-dev/pytest/pull/1281 - """ + + +def _readline_workaround(): + """ + Ensure readline is imported so that it attaches to the correct stdio + handles on Windows. + + Pdb uses readline support where available--when not running from the Python + prompt, the readline module is not imported until running the pdb REPL. If + running pytest with the --pdb option this means the readline module is not + imported until after I/O capture has been started. + + This is a problem for pyreadline, which is often used to implement readline + support on Windows, as it does not attach to the correct handles for stdout + and/or stdin if they have been redirected by the FDCapture mechanism. This + workaround ensures that readline is imported before I/O capture is setup so + that it can attach to the actual stdin/out for the console. + + See https://github.com/pytest-dev/pytest/pull/1281 + """ if sys.platform.startswith("win32"): try: import readline # noqa: F401 except ImportError: pass - - -def _py36_windowsconsoleio_workaround(stream): - """ - Python 3.6 implemented unicode console handling for Windows. This works - by reading/writing to the raw console handle using - ``{Read,Write}ConsoleW``. - - The problem is that we are going to ``dup2`` over the stdio file - descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the - handles used by Python to write to the console. Though there is still some - weirdness and the console handle seems to only be closed randomly and not - on the first call to ``CloseHandle``, or maybe it gets reopened with the - same handle value when we suspend capturing. - - The workaround in this case will reopen stdio with a different fd which - also means a different handle by replicating the logic in - "Py_lifecycle.c:initstdio/create_stdio". - - :param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given - here as parameter for unittesting purposes. - - See https://github.com/pytest-dev/py/issues/103 - """ - if not sys.platform.startswith("win32") or sys.version_info[:2] < (3, 6): - return - - # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666) - if not hasattr(stream, "buffer"): - return - - buffered = hasattr(stream.buffer, "raw") - raw_stdout = stream.buffer.raw if buffered else stream.buffer - - if not isinstance(raw_stdout, io._WindowsConsoleIO): - return - - def _reopen_stdio(f, mode): - if not buffered and mode[0] == "w": - buffering = 0 - else: - buffering = -1 - - return io.TextIOWrapper( - open(os.dup(f.fileno()), mode, buffering), - f.encoding, - f.errors, - f.newlines, - f.line_buffering, - ) - + + +def _py36_windowsconsoleio_workaround(stream): + """ + Python 3.6 implemented unicode console handling for Windows. This works + by reading/writing to the raw console handle using + ``{Read,Write}ConsoleW``. + + The problem is that we are going to ``dup2`` over the stdio file + descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the + handles used by Python to write to the console. Though there is still some + weirdness and the console handle seems to only be closed randomly and not + on the first call to ``CloseHandle``, or maybe it gets reopened with the + same handle value when we suspend capturing. + + The workaround in this case will reopen stdio with a different fd which + also means a different handle by replicating the logic in + "Py_lifecycle.c:initstdio/create_stdio". + + :param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given + here as parameter for unittesting purposes. + + See https://github.com/pytest-dev/py/issues/103 + """ + if not sys.platform.startswith("win32") or sys.version_info[:2] < (3, 6): + return + + # bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666) + if not hasattr(stream, "buffer"): + return + + buffered = hasattr(stream.buffer, "raw") + raw_stdout = stream.buffer.raw if buffered else stream.buffer + + if not isinstance(raw_stdout, io._WindowsConsoleIO): + return + + def _reopen_stdio(f, mode): + if not buffered and mode[0] == "w": + buffering = 0 + else: + buffering = -1 + + return io.TextIOWrapper( + open(os.dup(f.fileno()), mode, buffering), + f.encoding, + f.errors, + f.newlines, + f.line_buffering, + ) + sys.stdin = _reopen_stdio(sys.stdin, "rb") sys.stdout = _reopen_stdio(sys.stdout, "wb") sys.stderr = _reopen_stdio(sys.stderr, "wb") - - -def _attempt_to_close_capture_file(f): - """Suppress IOError when closing the temporary file used for capturing streams in py27 (#2370)""" - if six.PY2: - try: - f.close() - except IOError: - pass - else: - f.close() + + +def _attempt_to_close_capture_file(f): + """Suppress IOError when closing the temporary file used for capturing streams in py27 (#2370)""" + if six.PY2: + try: + f.close() + except IOError: + pass + else: + f.close() diff --git a/contrib/python/pytest/py2/_pytest/compat.py b/contrib/python/pytest/py2/_pytest/compat.py index a4b30ad550..e5c3b84667 100644 --- a/contrib/python/pytest/py2/_pytest/compat.py +++ b/contrib/python/pytest/py2/_pytest/compat.py @@ -1,195 +1,195 @@ # -*- coding: utf-8 -*- -""" -python version compatibility code -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import codecs -import functools -import inspect -import re -import sys -from contextlib import contextmanager - +""" +python version compatibility code +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import codecs +import functools +import inspect +import re +import sys +from contextlib import contextmanager + import attr -import py -import six -from six import text_type - -import _pytest +import py +import six +from six import text_type + +import _pytest from _pytest._io.saferepr import saferepr -from _pytest.outcomes import fail -from _pytest.outcomes import TEST_OUTCOME - -try: - import enum -except ImportError: # pragma: no cover - # Only available in Python 3.4+ or as a backport - enum = None - -_PY3 = sys.version_info > (3, 0) -_PY2 = not _PY3 - - -if _PY3: - from inspect import signature, Parameter as Parameter -else: - from funcsigs import signature, Parameter as Parameter - -NOTSET = object() - -PY35 = sys.version_info[:2] >= (3, 5) -PY36 = sys.version_info[:2] >= (3, 6) -MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError" - - -if _PY3: - from collections.abc import MutableMapping as MappingMixin +from _pytest.outcomes import fail +from _pytest.outcomes import TEST_OUTCOME + +try: + import enum +except ImportError: # pragma: no cover + # Only available in Python 3.4+ or as a backport + enum = None + +_PY3 = sys.version_info > (3, 0) +_PY2 = not _PY3 + + +if _PY3: + from inspect import signature, Parameter as Parameter +else: + from funcsigs import signature, Parameter as Parameter + +NOTSET = object() + +PY35 = sys.version_info[:2] >= (3, 5) +PY36 = sys.version_info[:2] >= (3, 6) +MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError" + + +if _PY3: + from collections.abc import MutableMapping as MappingMixin from collections.abc import Iterable, Mapping, Sequence, Sized -else: - # those raise DeprecationWarnings in Python >=3.7 - from collections import MutableMapping as MappingMixin # noqa +else: + # those raise DeprecationWarnings in Python >=3.7 + from collections import MutableMapping as MappingMixin # noqa from collections import Iterable, Mapping, Sequence, Sized # noqa - - -if sys.version_info >= (3, 4): - from importlib.util import spec_from_file_location -else: - - def spec_from_file_location(*_, **__): - return None - - + + +if sys.version_info >= (3, 4): + from importlib.util import spec_from_file_location +else: + + def spec_from_file_location(*_, **__): + return None + + if sys.version_info >= (3, 8): from importlib import metadata as importlib_metadata # noqa else: import importlib_metadata # noqa -def _format_args(func): - return str(signature(func)) - - -isfunction = inspect.isfunction -isclass = inspect.isclass -# used to work around a python2 exception info leak -exc_clear = getattr(sys, "exc_clear", lambda: None) -# The type of re.compile objects is not exposed in Python. -REGEX_TYPE = type(re.compile("")) - - -def is_generator(func): - genfunc = inspect.isgeneratorfunction(func) - return genfunc and not iscoroutinefunction(func) - - -def iscoroutinefunction(func): - """Return True if func is a decorated coroutine function. - - Note: copied and modified from Python 3.5's builtin couroutines.py to avoid import asyncio directly, - which in turns also initializes the "logging" module as side-effect (see issue #8). - """ - return getattr(func, "_is_coroutine", False) or ( - hasattr(inspect, "iscoroutinefunction") and inspect.iscoroutinefunction(func) - ) - - -def getlocation(function, curdir): - function = get_real_func(function) - fn = py.path.local(inspect.getfile(function)) - lineno = function.__code__.co_firstlineno - if fn.relto(curdir): - fn = fn.relto(curdir) - return "%s:%d" % (fn, lineno + 1) - - -def num_mock_patch_args(function): - """ return number of arguments used up by mock arguments (if any) """ - patchings = getattr(function, "patchings", None) - if not patchings: - return 0 - mock_modules = [sys.modules.get("mock"), sys.modules.get("unittest.mock")] - if any(mock_modules): - sentinels = [m.DEFAULT for m in mock_modules if m is not None] - return len( - [p for p in patchings if not p.attribute_name and p.new in sentinels] - ) - return len(patchings) - - -def getfuncargnames(function, is_method=False, cls=None): - """Returns the names of a function's mandatory arguments. - - This 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. - - @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( - "Could not determine arguments of {!r}: {}".format(function, 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 - ) - # 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(function.__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 - - -@contextmanager -def dummy_context_manager(): - """Context manager that does nothing, useful in situations where you might need an actual context manager or not - depending on some condition. Using this allow to keep the same code""" - yield - - -def get_default_arg_names(function): - # 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 - ) - - +def _format_args(func): + return str(signature(func)) + + +isfunction = inspect.isfunction +isclass = inspect.isclass +# used to work around a python2 exception info leak +exc_clear = getattr(sys, "exc_clear", lambda: None) +# The type of re.compile objects is not exposed in Python. +REGEX_TYPE = type(re.compile("")) + + +def is_generator(func): + genfunc = inspect.isgeneratorfunction(func) + return genfunc and not iscoroutinefunction(func) + + +def iscoroutinefunction(func): + """Return True if func is a decorated coroutine function. + + Note: copied and modified from Python 3.5's builtin couroutines.py to avoid import asyncio directly, + which in turns also initializes the "logging" module as side-effect (see issue #8). + """ + return getattr(func, "_is_coroutine", False) or ( + hasattr(inspect, "iscoroutinefunction") and inspect.iscoroutinefunction(func) + ) + + +def getlocation(function, curdir): + function = get_real_func(function) + fn = py.path.local(inspect.getfile(function)) + lineno = function.__code__.co_firstlineno + if fn.relto(curdir): + fn = fn.relto(curdir) + return "%s:%d" % (fn, lineno + 1) + + +def num_mock_patch_args(function): + """ return number of arguments used up by mock arguments (if any) """ + patchings = getattr(function, "patchings", None) + if not patchings: + return 0 + mock_modules = [sys.modules.get("mock"), sys.modules.get("unittest.mock")] + if any(mock_modules): + sentinels = [m.DEFAULT for m in mock_modules if m is not None] + return len( + [p for p in patchings if not p.attribute_name and p.new in sentinels] + ) + return len(patchings) + + +def getfuncargnames(function, is_method=False, cls=None): + """Returns the names of a function's mandatory arguments. + + This 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. + + @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( + "Could not determine arguments of {!r}: {}".format(function, 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 + ) + # 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(function.__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 + + +@contextmanager +def dummy_context_manager(): + """Context manager that does nothing, useful in situations where you might need an actual context manager or not + depending on some condition. Using this allow to keep the same code""" + yield + + +def get_default_arg_names(function): + # 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 + ) + + _non_printable_ascii_translate_table = { i: u"\\x{:02x}".format(i) for i in range(128) if i not in range(32, 127) } @@ -202,269 +202,269 @@ def _translate_non_printable(s): return s.translate(_non_printable_ascii_translate_table) -if _PY3: - STRING_TYPES = bytes, str - UNICODE_TYPES = six.text_type - - if PY35: - - def _bytes_to_ascii(val): - return val.decode("ascii", "backslashreplace") - - else: - - def _bytes_to_ascii(val): - if val: - # source: http://goo.gl/bGsnwC - encoded_bytes, _ = codecs.escape_encode(val) - return encoded_bytes.decode("ascii") - else: - # empty bytes crashes codecs.escape_encode (#1087) - return "" - - def ascii_escaped(val): - """If val is pure ascii, returns it as a str(). Otherwise, escapes - bytes objects into a sequence of escaped bytes: - - b'\xc3\xb4\xc5\xd6' -> u'\\xc3\\xb4\\xc5\\xd6' - - and escapes unicode objects into a sequence of escaped unicode - ids, e.g.: - - '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 - want to return escaped bytes for any byte, even if they match - a utf-8 string. - - """ - if isinstance(val, bytes): +if _PY3: + STRING_TYPES = bytes, str + UNICODE_TYPES = six.text_type + + if PY35: + + def _bytes_to_ascii(val): + return val.decode("ascii", "backslashreplace") + + else: + + def _bytes_to_ascii(val): + if val: + # source: http://goo.gl/bGsnwC + encoded_bytes, _ = codecs.escape_encode(val) + return encoded_bytes.decode("ascii") + else: + # empty bytes crashes codecs.escape_encode (#1087) + return "" + + def ascii_escaped(val): + """If val is pure ascii, returns it as a str(). Otherwise, escapes + bytes objects into a sequence of escaped bytes: + + b'\xc3\xb4\xc5\xd6' -> u'\\xc3\\xb4\\xc5\\xd6' + + and escapes unicode objects into a sequence of escaped unicode + ids, e.g.: + + '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 + want to return escaped bytes for any byte, even if they match + a utf-8 string. + + """ + if isinstance(val, bytes): ret = _bytes_to_ascii(val) - else: + else: ret = val return ret - - -else: - STRING_TYPES = six.string_types - UNICODE_TYPES = six.text_type - - def ascii_escaped(val): - """In py2 bytes and str are the same type, so return if it's a bytes - object, return it unchanged if it is a full ascii string, - otherwise escape it into its binary form. - - If it's a unicode string, change the unicode characters into - unicode escapes. - - """ - if isinstance(val, bytes): - try: + + +else: + STRING_TYPES = six.string_types + UNICODE_TYPES = six.text_type + + def ascii_escaped(val): + """In py2 bytes and str are the same type, so return if it's a bytes + object, return it unchanged if it is a full ascii string, + otherwise escape it into its binary form. + + If it's a unicode string, change the unicode characters into + unicode escapes. + + """ + if isinstance(val, bytes): + try: ret = val.decode("utf-8") - except UnicodeDecodeError: + except UnicodeDecodeError: ret = val.decode("utf-8", "ignore") - else: + else: ret = val.encode("utf-8", "replace").decode("utf-8") return ret - - -class _PytestWrapper(object): - """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. - """ - - def __init__(self, obj): - self.obj = obj - - -def get_real_func(obj): - """ gets 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: - raise ValueError( - ("could not find real function of {start}\nstopped at {current}").format( + + +class _PytestWrapper(object): + """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. + """ + + def __init__(self, obj): + self.obj = obj + + +def get_real_func(obj): + """ gets 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: + 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): - """ - Attempts 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) - except Exception: - return obj - if is_method and hasattr(obj, "__get__") and callable(obj.__get__): - obj = obj.__get__(holder) - return obj - - -def getfslineno(obj): - # xxx let decorators etc specify a sane ordering - obj = get_real_func(obj) - if hasattr(obj, "place_as"): - obj = obj.place_as - fslineno = _pytest._code.getfslineno(obj) - assert isinstance(fslineno[1], int), obj - return fslineno - - -def getimfunc(func): - try: - return func.__func__ - except AttributeError: - return func - - -def safe_getattr(object, name, default): - """ Like getattr but return default upon any Exception or any OutcomeException. - - 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 - - -def safe_isclass(obj): - """Ignore any exception via isinstance on Python 3.""" - try: - return isclass(obj) - except Exception: - return False - - -def _is_unittest_unexpected_success_a_failure(): - """Return if the test suite should fail if an @expectedFailure unittest test PASSES. - + ) + ) + if isinstance(obj, functools.partial): + obj = obj.func + return obj + + +def get_real_method(obj, holder): + """ + Attempts 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) + except Exception: + return obj + if is_method and hasattr(obj, "__get__") and callable(obj.__get__): + obj = obj.__get__(holder) + return obj + + +def getfslineno(obj): + # xxx let decorators etc specify a sane ordering + obj = get_real_func(obj) + if hasattr(obj, "place_as"): + obj = obj.place_as + fslineno = _pytest._code.getfslineno(obj) + assert isinstance(fslineno[1], int), obj + return fslineno + + +def getimfunc(func): + try: + return func.__func__ + except AttributeError: + return func + + +def safe_getattr(object, name, default): + """ Like getattr but return default upon any Exception or any OutcomeException. + + 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 + + +def safe_isclass(obj): + """Ignore any exception via isinstance on Python 3.""" + try: + return isclass(obj) + except Exception: + return False + + +def _is_unittest_unexpected_success_a_failure(): + """Return if the test suite should fail if an @expectedFailure unittest test PASSES. + From https://docs.python.org/3/library/unittest.html?highlight=unittest#unittest.TestResult.wasSuccessful: - Changed in version 3.4: Returns False if there were any - unexpectedSuccesses from tests marked with the expectedFailure() decorator. - """ - return sys.version_info >= (3, 4) - - -if _PY3: - - def safe_str(v): - """returns v as string""" + Changed in version 3.4: Returns False if there were any + unexpectedSuccesses from tests marked with the expectedFailure() decorator. + """ + return sys.version_info >= (3, 4) + + +if _PY3: + + def safe_str(v): + """returns v as string""" try: return str(v) except UnicodeEncodeError: return str(v, encoding="utf-8") - - -else: - - def safe_str(v): + + +else: + + def safe_str(v): """returns v as string, converting to utf-8 if necessary""" - try: - return str(v) - except UnicodeError: - if not isinstance(v, text_type): - v = text_type(v) - errors = "replace" - return v.encode("utf-8", errors) - - -COLLECT_FAKEMODULE_ATTRIBUTES = ( - "Collector", - "Module", - "Function", - "Instance", - "Session", - "Item", - "Class", - "File", - "_fillfuncargs", -) - - -def _setup_collect_fakemodule(): - from types import ModuleType - import pytest - - pytest.collect = ModuleType("pytest.collect") - pytest.collect.__all__ = [] # used for setns + try: + return str(v) + except UnicodeError: + if not isinstance(v, text_type): + v = text_type(v) + errors = "replace" + return v.encode("utf-8", errors) + + +COLLECT_FAKEMODULE_ATTRIBUTES = ( + "Collector", + "Module", + "Function", + "Instance", + "Session", + "Item", + "Class", + "File", + "_fillfuncargs", +) + + +def _setup_collect_fakemodule(): + from types import ModuleType + import pytest + + pytest.collect = ModuleType("pytest.collect") + pytest.collect.__all__ = [] # used for setns for attribute in COLLECT_FAKEMODULE_ATTRIBUTES: setattr(pytest.collect, attribute, getattr(pytest, attribute)) - - -if _PY2: - # Without this the test_dupfile_on_textio will fail, otherwise CaptureIO could directly inherit from StringIO. - from py.io import TextIO - - class CaptureIO(TextIO): - @property - def encoding(self): - return getattr(self, "_encoding", "UTF-8") - - -else: - import io - - class CaptureIO(io.TextIOWrapper): - def __init__(self): - super(CaptureIO, self).__init__( - io.BytesIO(), encoding="UTF-8", newline="", write_through=True - ) - - def getvalue(self): - return self.buffer.getvalue().decode("UTF-8") - - -class FuncargnamesCompatAttr(object): - """ helper class so that Metafunc, Function and FixtureRequest - don't need to each define the "funcargnames" compatibility attribute. - """ - - @property - def funcargnames(self): - """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" - return self.fixturenames - - -if six.PY2: - - def lru_cache(*_, **__): - def dec(fn): - return fn - - return dec - - -else: - from functools import lru_cache # noqa: F401 + + +if _PY2: + # Without this the test_dupfile_on_textio will fail, otherwise CaptureIO could directly inherit from StringIO. + from py.io import TextIO + + class CaptureIO(TextIO): + @property + def encoding(self): + return getattr(self, "_encoding", "UTF-8") + + +else: + import io + + class CaptureIO(io.TextIOWrapper): + def __init__(self): + super(CaptureIO, self).__init__( + io.BytesIO(), encoding="UTF-8", newline="", write_through=True + ) + + def getvalue(self): + return self.buffer.getvalue().decode("UTF-8") + + +class FuncargnamesCompatAttr(object): + """ helper class so that Metafunc, Function and FixtureRequest + don't need to each define the "funcargnames" compatibility attribute. + """ + + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + return self.fixturenames + + +if six.PY2: + + def lru_cache(*_, **__): + def dec(fn): + return fn + + return dec + + +else: + from functools import lru_cache # noqa: F401 if getattr(attr, "__version_info__", ()) >= (19, 2): diff --git a/contrib/python/pytest/py2/_pytest/config/__init__.py b/contrib/python/pytest/py2/_pytest/config/__init__.py index 52acbccf7b..0737ff9d51 100644 --- a/contrib/python/pytest/py2/_pytest/config/__init__.py +++ b/contrib/python/pytest/py2/_pytest/config/__init__.py @@ -1,164 +1,164 @@ # -*- coding: utf-8 -*- -""" command line options, ini-file and conftest.py processing. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import argparse -import copy -import inspect -import os -import shlex -import sys -import types -import warnings - +""" command line options, ini-file and conftest.py processing. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import copy +import inspect +import os +import shlex +import sys +import types +import warnings + import attr -import py -import six +import py +import six from packaging.version import Version -from pluggy import HookimplMarker -from pluggy import HookspecMarker -from pluggy import PluginManager - -import _pytest._code -import _pytest.assertion -import _pytest.hookspec # the extension point definitions -from .exceptions import PrintHelp -from .exceptions import UsageError -from .findpaths import determine_setup -from .findpaths import exists +from pluggy import HookimplMarker +from pluggy import HookspecMarker +from pluggy import PluginManager + +import _pytest._code +import _pytest.assertion +import _pytest.hookspec # the extension point definitions +from .exceptions import PrintHelp +from .exceptions import UsageError +from .findpaths import determine_setup +from .findpaths import exists from _pytest import deprecated -from _pytest._code import ExceptionInfo -from _pytest._code import filter_traceback +from _pytest._code import ExceptionInfo +from _pytest._code import filter_traceback from _pytest.compat import importlib_metadata -from _pytest.compat import lru_cache -from _pytest.compat import safe_str +from _pytest.compat import lru_cache +from _pytest.compat import safe_str from _pytest.outcomes import fail -from _pytest.outcomes import Skipped +from _pytest.outcomes import Skipped from _pytest.pathlib import Path from _pytest.warning_types import PytestConfigWarning - -hookimpl = HookimplMarker("pytest") -hookspec = HookspecMarker("pytest") - - -class ConftestImportFailure(Exception): - def __init__(self, path, excinfo): - Exception.__init__(self, path, excinfo) - self.path = path - self.excinfo = excinfo - - -def main(args=None, plugins=None): - """ return exit code, after performing an in-process test run. - - :arg args: list of command line arguments. - - :arg plugins: list of plugin objects to be auto-registered during - initialization. - """ - from _pytest.main import EXIT_USAGEERROR - - try: - try: - config = _prepareconfig(args, plugins) - except ConftestImportFailure as e: - exc_info = ExceptionInfo(e.excinfo) - tw = py.io.TerminalWriter(sys.stderr) - tw.line( - "ImportError while loading conftest '{e.path}'.".format(e=e), red=True - ) - exc_info.traceback = exc_info.traceback.filter(filter_traceback) - exc_repr = ( - exc_info.getrepr(style="short", chain=False) - if exc_info.traceback - else exc_info.exconly() - ) - formatted_tb = safe_str(exc_repr) - for line in formatted_tb.splitlines(): - tw.line(line.rstrip(), red=True) - return 4 - else: - try: - return config.hook.pytest_cmdline_main(config=config) - finally: - config._ensure_unconfigure() - except UsageError as e: - tw = py.io.TerminalWriter(sys.stderr) - for msg in e.args: - tw.line("ERROR: {}\n".format(msg), red=True) - return EXIT_USAGEERROR - - -class cmdline(object): # compatibility namespace - main = staticmethod(main) - - -def filename_arg(path, optname): - """ Argparse type validator for filename arguments. - - :path: path of filename - :optname: name of the option - """ - if os.path.isdir(path): - raise UsageError("{} must be a filename, given: {}".format(optname, path)) - return path - - -def directory_arg(path, optname): - """Argparse type validator for directory arguments. - - :path: path of directory - :optname: name of the option - """ - if not os.path.isdir(path): - raise UsageError("{} must be a directory, given: {}".format(optname, path)) - return path - - + +hookimpl = HookimplMarker("pytest") +hookspec = HookspecMarker("pytest") + + +class ConftestImportFailure(Exception): + def __init__(self, path, excinfo): + Exception.__init__(self, path, excinfo) + self.path = path + self.excinfo = excinfo + + +def main(args=None, plugins=None): + """ return exit code, after performing an in-process test run. + + :arg args: list of command line arguments. + + :arg plugins: list of plugin objects to be auto-registered during + initialization. + """ + from _pytest.main import EXIT_USAGEERROR + + try: + try: + config = _prepareconfig(args, plugins) + except ConftestImportFailure as e: + exc_info = ExceptionInfo(e.excinfo) + tw = py.io.TerminalWriter(sys.stderr) + tw.line( + "ImportError while loading conftest '{e.path}'.".format(e=e), red=True + ) + exc_info.traceback = exc_info.traceback.filter(filter_traceback) + exc_repr = ( + exc_info.getrepr(style="short", chain=False) + if exc_info.traceback + else exc_info.exconly() + ) + formatted_tb = safe_str(exc_repr) + for line in formatted_tb.splitlines(): + tw.line(line.rstrip(), red=True) + return 4 + else: + try: + return config.hook.pytest_cmdline_main(config=config) + finally: + config._ensure_unconfigure() + except UsageError as e: + tw = py.io.TerminalWriter(sys.stderr) + for msg in e.args: + tw.line("ERROR: {}\n".format(msg), red=True) + return EXIT_USAGEERROR + + +class cmdline(object): # compatibility namespace + main = staticmethod(main) + + +def filename_arg(path, optname): + """ Argparse type validator for filename arguments. + + :path: path of filename + :optname: name of the option + """ + if os.path.isdir(path): + raise UsageError("{} must be a filename, given: {}".format(optname, path)) + return path + + +def directory_arg(path, optname): + """Argparse type validator for directory arguments. + + :path: path of directory + :optname: name of the option + """ + if not os.path.isdir(path): + raise UsageError("{} must be a directory, given: {}".format(optname, 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", - "resultlog", - "doctest", - "cacheprovider", - "freeze_support", - "setuponly", - "setupplan", - "stepwise", - "warnings", - "logging", + "debugging", + "unittest", + "capture", + "skipping", + "tmpdir", + "monkeypatch", + "recwarn", + "pastebin", + "nose", + "assertion", + "junitxml", + "resultlog", + "doctest", + "cacheprovider", + "freeze_support", + "setuponly", + "setupplan", + "stepwise", + "warnings", + "logging", "reports", -) - -builtin_plugins = set(default_plugins) -builtin_plugins.add("pytester") - - +) + +builtin_plugins = set(default_plugins) +builtin_plugins.add("pytester") + + def get_config(args=None, plugins=None): - # 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( @@ -170,323 +170,323 @@ def get_config(args=None, plugins=None): # Handle any "-p no:plugin" args. pluginmanager.consider_preparse(args) - for spec in default_plugins: - pluginmanager.import_plugin(spec) - return config - - -def get_plugin_manager(): - """ - 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 - - -def _prepareconfig(args=None, plugins=None): - warning = None - if args is None: - args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] - elif not isinstance(args, (tuple, list)): + for spec in default_plugins: + pluginmanager.import_plugin(spec) + return config + + +def get_plugin_manager(): + """ + 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 + + +def _prepareconfig(args=None, plugins=None): + warning = None + if args is None: + args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] + elif not isinstance(args, (tuple, list)): msg = "`args` parameter expected to be a list or tuple 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: - if isinstance(plugin, six.string_types): - pluginmanager.consider_pluginarg(plugin) - else: - pluginmanager.register(plugin) - if warning: + pluginmanager = config.pluginmanager + try: + if plugins: + for plugin in plugins: + if isinstance(plugin, six.string_types): + pluginmanager.consider_pluginarg(plugin) + else: + pluginmanager.register(plugin) + if warning: from _pytest.warnings import _issue_warning_captured - + _issue_warning_captured(warning, hook=config.hook, stacklevel=4) - return pluginmanager.hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args - ) - except BaseException: - config._ensure_unconfigure() - raise - - -class PytestPluginManager(PluginManager): - """ - Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add 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): - super(PytestPluginManager, self).__init__("pytest") - self._conftest_plugins = set() - - # state related to local conftest plugins - self._dirpath2confmods = {} - self._conftestpath2mod = {} - self._confcutdir = None - self._noconftest = False - self._duplicatepaths = set() - - self.add_hookspecs(_pytest.hookspec) - self.register(self) - if os.environ.get("PYTEST_DEBUG"): - err = sys.stderr - encoding = getattr(err, "encoding", "utf8") - try: - err = py.io.dupfile(err, 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() - # Used to know when we are importing conftests after the pytest_configure stage - self._configured = False - - def addhooks(self, module_or_class): - """ - .. deprecated:: 2.8 - - Use :py:meth:`pluggy.PluginManager.add_hookspecs <PluginManager.add_hookspecs>` - instead. - """ + return pluginmanager.hook.pytest_cmdline_parse( + pluginmanager=pluginmanager, args=args + ) + except BaseException: + config._ensure_unconfigure() + raise + + +class PytestPluginManager(PluginManager): + """ + Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add 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): + super(PytestPluginManager, self).__init__("pytest") + self._conftest_plugins = set() + + # state related to local conftest plugins + self._dirpath2confmods = {} + self._conftestpath2mod = {} + self._confcutdir = None + self._noconftest = False + self._duplicatepaths = set() + + self.add_hookspecs(_pytest.hookspec) + self.register(self) + if os.environ.get("PYTEST_DEBUG"): + err = sys.stderr + encoding = getattr(err, "encoding", "utf8") + try: + err = py.io.dupfile(err, 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() + # Used to know when we are importing conftests after the pytest_configure stage + self._configured = False + + def addhooks(self, module_or_class): + """ + .. deprecated:: 2.8 + + Use :py:meth:`pluggy.PluginManager.add_hookspecs <PluginManager.add_hookspecs>` + instead. + """ warnings.warn(deprecated.PLUGIN_MANAGER_ADDHOOKS, stacklevel=2) - return self.add_hookspecs(module_or_class) - - def parse_hookimpl_opts(self, plugin, name): - # pytest hooks are always prefixed with pytest_ - # so we avoid accessing possibly non-readable attributes - # (see issue #1073) - if not name.startswith("pytest_"): - return + return self.add_hookspecs(module_or_class) + + def parse_hookimpl_opts(self, plugin, name): + # pytest hooks are always prefixed with pytest_ + # so we avoid accessing possibly non-readable attributes + # (see issue #1073) + if not name.startswith("pytest_"): + return # ignore names which can not be hooks if name == "pytest_plugins": - return - - method = getattr(plugin, name) - opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name) - - # consider only actual functions for hooks (#3775) - 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 = {} + return + + method = getattr(plugin, name) + opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name) + + # consider only actual functions for hooks (#3775) + 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 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 - - def parse_hookspec_opts(self, module_or_class, name): - opts = super(PytestPluginManager, self).parse_hookspec_opts( - module_or_class, name - ) - if opts is None: - method = getattr(module_or_class, name) - - if name.startswith("pytest_"): + return opts + + def parse_hookspec_opts(self, module_or_class, name): + opts = super(PytestPluginManager, self).parse_hookspec_opts( + module_or_class, name + ) + if opts is None: + method = getattr(module_or_class, name) + + 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 - - def register(self, plugin, name=None): - if name in ["pytest_catchlog", "pytest_capturelog"]: + } + return opts + + def register(self, plugin, name=None): + if name in ["pytest_catchlog", "pytest_capturelog"]: warnings.warn( PytestConfigWarning( "{} plugin has been merged into the core, " "please remove it from your requirements.".format( name.replace("_", "-") ) - ) - ) - return - ret = super(PytestPluginManager, self).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 - - def getplugin(self, name): - # support deprecated naming because plugins (xdist e.g.) use it - return self.get_plugin(name) - - def hasplugin(self, name): - """Return True if the plugin with the given name is registered.""" - return bool(self.get_plugin(name)) - - def pytest_configure(self, config): - # 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 - - # - # internal API for local conftest plugin handling - # - def _set_initial_conftests(self, namespace): - """ load initial conftest files given a preparsed "namespace". - As conftest files may add their own command line options - which have arguments ('--my-opt somepath') we might get some - 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 - for path in testpaths: - path = str(path) - # remove node-id syntax - i = path.find("::") - if i != -1: - path = path[:i] - anchor = current.join(path, abs=1) - if exists(anchor): # we found some file object - self._try_load_conftest(anchor) - foundanchor = True - if not foundanchor: - self._try_load_conftest(current) - - def _try_load_conftest(self, anchor): - self._getconftestmodules(anchor) - # 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) - - @lru_cache(maxsize=128) - def _getconftestmodules(self, path): - if self._noconftest: - return [] - - if path.isfile(): - directory = path.dirpath() - else: - directory = path - - if six.PY2: # py2 is not using lru_cache. - try: - return self._dirpath2confmods[directory] - except KeyError: - pass - - # XXX these days we may rather want to use config.rootdir - # and allow users to opt into looking into the rootdir parent - # directories instead of requiring to specify confcutdir - clist = [] - for parent in directory.realpath().parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.isfile(): + ) + ) + return + ret = super(PytestPluginManager, self).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 + + def getplugin(self, name): + # support deprecated naming because plugins (xdist e.g.) use it + return self.get_plugin(name) + + def hasplugin(self, name): + """Return True if the plugin with the given name is registered.""" + return bool(self.get_plugin(name)) + + def pytest_configure(self, config): + # 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 + + # + # internal API for local conftest plugin handling + # + def _set_initial_conftests(self, namespace): + """ load initial conftest files given a preparsed "namespace". + As conftest files may add their own command line options + which have arguments ('--my-opt somepath') we might get some + 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 + for path in testpaths: + path = str(path) + # remove node-id syntax + i = path.find("::") + if i != -1: + path = path[:i] + anchor = current.join(path, abs=1) + if exists(anchor): # we found some file object + self._try_load_conftest(anchor) + foundanchor = True + if not foundanchor: + self._try_load_conftest(current) + + def _try_load_conftest(self, anchor): + self._getconftestmodules(anchor) + # 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) + + @lru_cache(maxsize=128) + def _getconftestmodules(self, path): + if self._noconftest: + return [] + + if path.isfile(): + directory = path.dirpath() + else: + directory = path + + if six.PY2: # py2 is not using lru_cache. + try: + return self._dirpath2confmods[directory] + except KeyError: + pass + + # XXX these days we may rather want to use config.rootdir + # and allow users to opt into looking into the rootdir parent + # directories instead of requiring to specify confcutdir + clist = [] + for parent in directory.realpath().parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.isfile(): # Use realpath to avoid loading the same conftest twice # with build systems that create build directories containing # symlinks to actual files. mod = self._importconftest(conftestpath.realpath()) - clist.append(mod) - self._dirpath2confmods[directory] = clist - return clist - - def _rget_with_confmod(self, name, path): - modules = self._getconftestmodules(path) - for mod in reversed(modules): - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - - def _importconftest(self, conftestpath): - try: - return self._conftestpath2mod[conftestpath] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - try: - mod = conftestpath.pyimport() - if ( - hasattr(mod, "pytest_plugins") - and self._configured - and not self._using_pyargs - ): - from _pytest.deprecated import ( + clist.append(mod) + self._dirpath2confmods[directory] = clist + return clist + + def _rget_with_confmod(self, name, path): + modules = self._getconftestmodules(path) + for mod in reversed(modules): + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def _importconftest(self, conftestpath): + try: + return self._conftestpath2mod[conftestpath] + except KeyError: + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + try: + mod = conftestpath.pyimport() + if ( + hasattr(mod, "pytest_plugins") + and self._configured + and not self._using_pyargs + ): + from _pytest.deprecated import ( PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST, - ) - + ) + fail( PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.format( conftestpath, self._confcutdir ), pytrace=False, - ) - except Exception: - raise ConftestImportFailure(conftestpath, sys.exc_info()) - - self._conftest_plugins.add(mod) - self._conftestpath2mod[conftestpath] = mod - dirpath = conftestpath.dirpath() - if dirpath in self._dirpath2confmods: - for path, mods in self._dirpath2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - self.trace("loaded conftestmodule %r" % (mod)) - self.consider_conftest(mod) - return mod - - # - # API for bootstrapping plugin loading - # - # - - def consider_preparse(self, args): + ) + except Exception: + raise ConftestImportFailure(conftestpath, sys.exc_info()) + + self._conftest_plugins.add(mod) + self._conftestpath2mod[conftestpath] = mod + dirpath = conftestpath.dirpath() + if dirpath in self._dirpath2confmods: + for path, mods in self._dirpath2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self.trace("loaded conftestmodule %r" % (mod)) + self.consider_conftest(mod) + return mod + + # + # API for bootstrapping plugin loading + # + # + + def consider_preparse(self, args): i = 0 n = len(args) while i < n: @@ -504,22 +504,22 @@ class PytestPluginManager(PluginManager): else: continue self.consider_pluginarg(parg) - - def consider_pluginarg(self, arg): - if arg.startswith("no:"): - name = arg[3:] + + def consider_pluginarg(self, arg): + 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: + # 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: name = arg # Unblock the plugin. None indicates that it has been blocked. # There is no interface with pluggy for this. @@ -529,58 +529,58 @@ 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): - self.register(conftestmodule, name=conftestmodule.__file__) - - def consider_env(self): - self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) - - def consider_module(self, mod): - self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) - - def _import_plugin_specs(self, spec): - plugins = _get_plugin_specs_as_list(spec) - for import_spec in plugins: - self.import_plugin(import_spec) - + + def consider_conftest(self, conftestmodule): + self.register(conftestmodule, name=conftestmodule.__file__) + + def consider_env(self): + self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS")) + + def consider_module(self, mod): + self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) + + def _import_plugin_specs(self, spec): + plugins = _get_plugin_specs_as_list(spec) + for import_spec in plugins: + self.import_plugin(import_spec) + def import_plugin(self, modname, consider_entry_points=False): """ Imports a plugin with ``modname``. If ``consider_entry_points`` is True, entry point names are also considered to find a plugin. """ - # most often modname refers to builtin modules, e.g. "pytester", - # "terminal" or "capture". Those plugins are registered under their - # basename for historic purposes but must be imported with the - # _pytest prefix. + # 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. assert isinstance(modname, six.string_types), ( - "module name as text required, got %r" % modname - ) - modname = str(modname) - if self.is_blocked(modname) or self.get_plugin(modname) is not None: - return + "module name as text required, got %r" % modname + ) + modname = str(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: - new_exc_message = 'Error importing plugin "%s": %s' % ( - modname, - safe_str(e.args[0]), - ) + try: + __import__(importspec) + except ImportError as e: + new_exc_message = 'Error importing plugin "%s": %s' % ( + modname, + safe_str(e.args[0]), + ) new_exc = ImportError(new_exc_message) tb = sys.exc_info()[2] - + six.reraise(ImportError, new_exc, tb) - - except Skipped as e: + + except Skipped as e: from _pytest.warnings import _issue_warning_captured _issue_warning_captured( @@ -588,47 +588,47 @@ class PytestPluginManager(PluginManager): self.hook, stacklevel=1, ) - else: - mod = sys.modules[importspec] - self.register(mod, modname) - - -def _get_plugin_specs_as_list(specs): - """ - Parses a list of "plugin specs" and returns a list of plugin names. - - Plugin specs can be given as a list of strings separated by "," or already as a list/tuple in - which case it is returned as a list. Specs can also be `None` in which case an - empty list is returned. - """ + else: + mod = sys.modules[importspec] + self.register(mod, modname) + + +def _get_plugin_specs_as_list(specs): + """ + Parses a list of "plugin specs" and returns a list of plugin names. + + Plugin specs can be given as a list of strings separated by "," or already as a list/tuple in + which case it is returned as a list. Specs can also be `None` in which case an + empty list is returned. + """ if specs is not None and not isinstance(specs, types.ModuleType): if isinstance(specs, six.string_types): - specs = specs.split(",") if specs else [] - if not isinstance(specs, (list, tuple)): - raise UsageError( - "Plugin specs must be a ','-separated string or a " - "list/tuple of strings for plugin names. Given: %r" % specs - ) - return list(specs) - return [] - - -def _ensure_removed_sysmodule(modname): - try: - del sys.modules[modname] - except KeyError: - pass - - -class Notset(object): - def __repr__(self): - return "<NOTSET>" - - -notset = Notset() - - -def _iter_rewritable_modules(package_files): + specs = specs.split(",") if specs else [] + if not isinstance(specs, (list, tuple)): + raise UsageError( + "Plugin specs must be a ','-separated string or a " + "list/tuple of strings for plugin names. Given: %r" % specs + ) + return list(specs) + return [] + + +def _ensure_removed_sysmodule(modname): + try: + del sys.modules[modname] + except KeyError: + pass + + +class Notset(object): + def __repr__(self): + return "<NOTSET>" + + +notset = Notset() + + +def _iter_rewritable_modules(package_files): """ Given an iterable of file names in a source distribution, return the "names" that should be marked for assertion rewrite (for example the package "pytest_mock/__init__.py" should @@ -661,20 +661,20 @@ def _iter_rewritable_modules(package_files): """ 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 @@ -690,12 +690,12 @@ def _iter_rewritable_modules(package_files): if new_package_files: for _module in _iter_rewritable_modules(new_package_files): yield _module - -class Config(object): + +class Config(object): """ Access to configuration values, pluginmanager and plugin hooks. - + :ivar PytestPluginManager pluginmanager: the plugin manager handles plugin registration and hook invocation. :ivar argparse.Namespace option: access to command line option as attributes. @@ -734,67 +734,67 @@ class Config(object): args=(), plugins=None, dir=Path().resolve() ) - #: access to command line option as attributes. - #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead - self.option = argparse.Namespace() - + #: access to command line option as attributes. + #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead + self.option = argparse.Namespace() + self.invocation_params = invocation_params - _a = FILE_OR_DIR - self._parser = Parser( - usage="%%(prog)s [options] [%s] [%s] [...]" % (_a, _a), - processopt=self._processopt, - ) - #: a pluginmanager instance - self.pluginmanager = pluginmanager - self.trace = self.pluginmanager.trace.root.get("config") - self.hook = self.pluginmanager.hook - self._inicache = {} - self._override_ini = () - self._opt2dest = {} - self._cleanup = [] - self.pluginmanager.register(self, "pytestconfig") - self._configured = False + _a = FILE_OR_DIR + self._parser = Parser( + usage="%%(prog)s [options] [%s] [%s] [...]" % (_a, _a), + processopt=self._processopt, + ) + #: a pluginmanager instance + self.pluginmanager = pluginmanager + self.trace = self.pluginmanager.trace.root.get("config") + self.hook = self.pluginmanager.hook + self._inicache = {} + self._override_ini = () + self._opt2dest = {} + self._cleanup = [] + self.pluginmanager.register(self, "pytestconfig") + self._configured = False self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) - + @property def invocation_dir(self): """Backward compatibility""" return py.path.local(str(self.invocation_params.dir)) - - def add_cleanup(self, func): - """ Add a function to be called when the config object gets out of - use (usually coninciding with pytest_unconfigure).""" - self._cleanup.append(func) - - def _do_configure(self): - assert not self._configured - self._configured = True - self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) - - def _ensure_unconfigure(self): - 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 add_cleanup(self, func): + """ Add a function to be called when the config object gets out of + use (usually coninciding with pytest_unconfigure).""" + self._cleanup.append(func) + + def _do_configure(self): + assert not self._configured + self._configured = True + self.hook.pytest_configure.call_historic(kwargs=dict(config=self)) + + def _ensure_unconfigure(self): + 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): return self.pluginmanager.get_plugin("terminalreporter")._tw - + def pytest_cmdline_parse(self, pluginmanager, args): 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 @@ -803,111 +803,111 @@ class Config(object): sys.stdout.write( "\nNOTE: displaying only minimal help due to UsageError.\n\n" ) - + raise - - return self - - def notify_exception(self, excinfo, option=None): + + return self + + def notify_exception(self, excinfo, option=None): if option and getattr(option, "fulltrace", False): - style = "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() - - def cwd_relative_nodeid(self, nodeid): - # nodeid's are relative to the rootpath, compute relative to cwd - if self.invocation_dir != self.rootdir: - fullpath = self.rootdir.join(nodeid) - nodeid = self.invocation_dir.bestrelpath(fullpath) - return nodeid - - @classmethod - def fromdictargs(cls, option_dict, args): - """ constructor useable for subprocesses. """ + style = "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() + + def cwd_relative_nodeid(self, nodeid): + # nodeid's are relative to the rootpath, compute relative to cwd + if self.invocation_dir != self.rootdir: + fullpath = self.rootdir.join(nodeid) + nodeid = self.invocation_dir.bestrelpath(fullpath) + return nodeid + + @classmethod + def fromdictargs(cls, option_dict, args): + """ constructor useable 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 - - def _processopt(self, opt): - for name in opt._short_opts + opt._long_opts: - self._opt2dest[name] = opt.dest - - if hasattr(opt, "default") and opt.dest: - 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): - self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) - - def _initini(self, args): - ns, unknown_args = self._parser.parse_known_and_unknown_args( - args, namespace=copy.copy(self.option) - ) - r = determine_setup( - ns.inifilename, - ns.file_or_dir + unknown_args, - rootdir_cmd_arg=ns.rootdir or None, - config=self, - ) - self.rootdir, self.inifile, self.inicfg = r - self._parser.extra_info["rootdir"] = self.rootdir - self._parser.extra_info["inifile"] = self.inifile - self._parser.addini("addopts", "extra command line options", "args") - self._parser.addini("minversion", "minimally required pytest version") - self._override_ini = ns.override_ini or () - - def _consider_importhook(self, 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) + 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): + for name in opt._short_opts + opt._long_opts: + self._opt2dest[name] = opt.dest + + if hasattr(opt, "default") and opt.dest: + 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): + self.pluginmanager._set_initial_conftests(early_config.known_args_namespace) + + def _initini(self, args): + ns, unknown_args = self._parser.parse_known_and_unknown_args( + args, namespace=copy.copy(self.option) + ) + r = determine_setup( + ns.inifilename, + ns.file_or_dir + unknown_args, + rootdir_cmd_arg=ns.rootdir or None, + config=self, + ) + self.rootdir, self.inifile, self.inicfg = r + self._parser.extra_info["rootdir"] = self.rootdir + self._parser.extra_info["inifile"] = self.inifile + self._parser.addini("addopts", "extra command line options", "args") + self._parser.addini("minversion", "minimally required pytest version") + self._override_ini = ns.override_ini or () + + def _consider_importhook(self, 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": - try: - hook = _pytest.assertion.install_importhook(self) - except SystemError: - mode = "plain" - else: - self._mark_plugins_for_rewrite(hook) - _warn_about_missing_assertion(mode) - - def _mark_plugins_for_rewrite(self, hook): - """ - Given an importhook, mark for rewrite any top-level - 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 = ( + if mode == "rewrite": + try: + hook = _pytest.assertion.install_importhook(self) + except SystemError: + mode = "plain" + else: + self._mark_plugins_for_rewrite(hook) + _warn_about_missing_assertion(mode) + + def _mark_plugins_for_rewrite(self, hook): + """ + Given an importhook, mark for rewrite any top-level + 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 = ( 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, via): """Validate known args.""" self._parser._config_source_hint = via @@ -920,43 +920,43 @@ class Config(object): return args - def _preparse(self, args, addopts=True): - if addopts: + def _preparse(self, args, addopts=True): + 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 ) - self._checkversion() - self._consider_importhook(args) - self.pluginmanager.consider_preparse(args) - 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 = ns = self._parser.parse_known_args( - args, namespace=copy.copy(self.option) - ) - if self.known_args_namespace.confcutdir is None and self.inifile: - confcutdir = py.path.local(self.inifile).dirname - self.known_args_namespace.confcutdir = confcutdir - try: - self.hook.pytest_load_initial_conftests( - early_config=self, args=args, parser=self._parser - ) - except ConftestImportFailure: - e = sys.exc_info()[1] - if ns.help or ns.version: - # we don't want to prevent --help/--version to work - # so just let is pass and print a warning at the end + self._checkversion() + self._consider_importhook(args) + self.pluginmanager.consider_preparse(args) + 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 = ns = self._parser.parse_known_args( + args, namespace=copy.copy(self.option) + ) + if self.known_args_namespace.confcutdir is None and self.inifile: + confcutdir = py.path.local(self.inifile).dirname + self.known_args_namespace.confcutdir = confcutdir + try: + self.hook.pytest_load_initial_conftests( + early_config=self, args=args, parser=self._parser + ) + except ConftestImportFailure: + e = sys.exc_info()[1] + if ns.help or ns.version: + # we don't want to prevent --help/--version to work + # so just let is pass and print a warning at the end from _pytest.warnings import _issue_warning_captured _issue_warning_captured( @@ -966,238 +966,238 @@ class Config(object): self.hook, stacklevel=2, ) - else: - raise - - def _checkversion(self): - import pytest - - minver = self.inicfg.get("minversion", None) - if minver: + else: + raise + + def _checkversion(self): + import pytest + + minver = self.inicfg.get("minversion", None) + if minver: if Version(minver) > Version(pytest.__version__): - raise pytest.UsageError( - "%s:%d: requires pytest-%s, actual pytest-%s'" - % ( - self.inicfg.config.path, - self.inicfg.lineof("minversion"), - minver, - pytest.__version__, - ) - ) - - def parse(self, args, addopts=True): - # 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._origargs = args - 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 - try: - args = self._parser.parse_setoption( - args, self.option, namespace=self.option - ) - if not args: - if self.invocation_dir == self.rootdir: - args = self.getini("testpaths") - if not args: - args = [str(self.invocation_dir)] - self.args = args - except PrintHelp: - pass - - def addinivalue_line(self, name, line): - """ 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 - the first line in its value. """ - x = self.getini(name) - assert isinstance(x, list) - x.append(line) # modifies the cached list inline - - def getini(self, name): - """ return configuration value from an :ref:`ini file <inifiles>`. If the - specified name hasn't been registered through a prior - :py:func:`parser.addini <_pytest.config.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 - - def _getini(self, name): - try: - description, type, default = self._parser._inidict[name] - except KeyError: - raise ValueError("unknown configuration value: %r" % (name,)) - value = self._get_override_ini_value(name) - if value is None: - try: - value = self.inicfg[name] - except KeyError: - if default is not None: - return default - if type is None: - return "" - return [] - if type == "pathlist": - dp = py.path.local(self.inicfg.config.path).dirpath() - values = [] - for relpath in shlex.split(value): - values.append(dp.join(relpath, abs=True)) - return values - elif type == "args": - return shlex.split(value) - elif type == "linelist": - return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] - elif type == "bool": - return bool(_strtobool(value.strip())) - else: - assert type is None - return value - - def _getconftest_pathlist(self, name, path): - try: - mod, relroots = self.pluginmanager._rget_with_confmod(name, path) - except KeyError: - return None - modpath = py.path.local(mod.__file__).dirpath() - values = [] - for relroot in relroots: - if not isinstance(relroot, py.path.local): - relroot = relroot.replace("/", py.path.local.sep) - relroot = modpath.join(relroot, abs=True) - values.append(relroot) - return values - - def _get_override_ini_value(self, name): - 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) - except ValueError: - raise UsageError("-o/--override-ini expects option=value style.") - else: - if key == name: - value = user_ini_value - return value - - def getoption(self, name, default=notset, skip=False): - """ return command line option value. - - :arg name: name of the option. You may also specify - the literal ``--OPT`` option instead of the "dest" option name. - :arg default: default value if no option of that name exists. - :arg 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 - except AttributeError: - if default is not notset: - return default - if skip: - import pytest - - pytest.skip("no %r option found" % (name,)) - raise ValueError("no option named %r" % (name,)) - - def getvalue(self, name, path=None): - """ (deprecated, use getoption()) """ - return self.getoption(name) - - def getvalueorskip(self, name, path=None): - """ (deprecated, use getoption(skip=True)) """ - return self.getoption(name, skip=True) - - -def _assertion_supported(): - try: - assert False - except AssertionError: - return True - else: - return False - - -def _warn_about_missing_assertion(mode): - if not _assertion_supported(): - if mode == "plain": - sys.stderr.write( - "WARNING: ASSERTIONS ARE NOT EXECUTED" - " and FAILING TESTS WILL PASS. Are you" - " using python -O?" - ) - else: - sys.stderr.write( - "WARNING: assertions not in test modules or" - " plugins will be ignored" - " because assert statements are not executed " - "by the underlying Python interpreter " - "(are you using python -O?)\n" - ) - - -def setns(obj, dic): - import pytest - - for name, value in dic.items(): - if isinstance(value, dict): - mod = getattr(obj, name, None) - if mod is None: - modname = "pytest.%s" % name - mod = types.ModuleType(modname) - sys.modules[modname] = mod - mod.__all__ = [] - setattr(obj, name, mod) - obj.__all__.append(name) - setns(mod, value) - else: - setattr(obj, name, value) - obj.__all__.append(name) - # if obj != pytest: - # pytest.__all__.append(name) - setattr(pytest, name, value) - - -def create_terminal_writer(config, *args, **kwargs): - """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 = py.io.TerminalWriter(*args, **kwargs) - if config.option.color == "yes": - tw.hasmarkup = True - if config.option.color == "no": - tw.hasmarkup = False - return tw - - -def _strtobool(val): - """Convert a string representation of truth to true (1) or false (0). - - 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"): - return 1 - elif val in ("n", "no", "f", "false", "off", "0"): - return 0 - else: - raise ValueError("invalid truth value %r" % (val,)) + raise pytest.UsageError( + "%s:%d: requires pytest-%s, actual pytest-%s'" + % ( + self.inicfg.config.path, + self.inicfg.lineof("minversion"), + minver, + pytest.__version__, + ) + ) + + def parse(self, args, addopts=True): + # 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._origargs = args + 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 + try: + args = self._parser.parse_setoption( + args, self.option, namespace=self.option + ) + if not args: + if self.invocation_dir == self.rootdir: + args = self.getini("testpaths") + if not args: + args = [str(self.invocation_dir)] + self.args = args + except PrintHelp: + pass + + def addinivalue_line(self, name, line): + """ 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 + the first line in its value. """ + x = self.getini(name) + assert isinstance(x, list) + x.append(line) # modifies the cached list inline + + def getini(self, name): + """ return configuration value from an :ref:`ini file <inifiles>`. If the + specified name hasn't been registered through a prior + :py:func:`parser.addini <_pytest.config.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 + + def _getini(self, name): + try: + description, type, default = self._parser._inidict[name] + except KeyError: + raise ValueError("unknown configuration value: %r" % (name,)) + value = self._get_override_ini_value(name) + if value is None: + try: + value = self.inicfg[name] + except KeyError: + if default is not None: + return default + if type is None: + return "" + return [] + if type == "pathlist": + dp = py.path.local(self.inicfg.config.path).dirpath() + values = [] + for relpath in shlex.split(value): + values.append(dp.join(relpath, abs=True)) + return values + elif type == "args": + return shlex.split(value) + elif type == "linelist": + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + elif type == "bool": + return bool(_strtobool(value.strip())) + else: + assert type is None + return value + + def _getconftest_pathlist(self, name, path): + try: + mod, relroots = self.pluginmanager._rget_with_confmod(name, path) + except KeyError: + return None + modpath = py.path.local(mod.__file__).dirpath() + values = [] + for relroot in relroots: + if not isinstance(relroot, py.path.local): + relroot = relroot.replace("/", py.path.local.sep) + relroot = modpath.join(relroot, abs=True) + values.append(relroot) + return values + + def _get_override_ini_value(self, name): + 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) + except ValueError: + raise UsageError("-o/--override-ini expects option=value style.") + else: + if key == name: + value = user_ini_value + return value + + def getoption(self, name, default=notset, skip=False): + """ return command line option value. + + :arg name: name of the option. You may also specify + the literal ``--OPT`` option instead of the "dest" option name. + :arg default: default value if no option of that name exists. + :arg 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 + except AttributeError: + if default is not notset: + return default + if skip: + import pytest + + pytest.skip("no %r option found" % (name,)) + raise ValueError("no option named %r" % (name,)) + + def getvalue(self, name, path=None): + """ (deprecated, use getoption()) """ + return self.getoption(name) + + def getvalueorskip(self, name, path=None): + """ (deprecated, use getoption(skip=True)) """ + return self.getoption(name, skip=True) + + +def _assertion_supported(): + try: + assert False + except AssertionError: + return True + else: + return False + + +def _warn_about_missing_assertion(mode): + if not _assertion_supported(): + if mode == "plain": + sys.stderr.write( + "WARNING: ASSERTIONS ARE NOT EXECUTED" + " and FAILING TESTS WILL PASS. Are you" + " using python -O?" + ) + else: + sys.stderr.write( + "WARNING: assertions not in test modules or" + " plugins will be ignored" + " because assert statements are not executed " + "by the underlying Python interpreter " + "(are you using python -O?)\n" + ) + + +def setns(obj, dic): + import pytest + + for name, value in dic.items(): + if isinstance(value, dict): + mod = getattr(obj, name, None) + if mod is None: + modname = "pytest.%s" % name + mod = types.ModuleType(modname) + sys.modules[modname] = mod + mod.__all__ = [] + setattr(obj, name, mod) + obj.__all__.append(name) + setns(mod, value) + else: + setattr(obj, name, value) + obj.__all__.append(name) + # if obj != pytest: + # pytest.__all__.append(name) + setattr(pytest, name, value) + + +def create_terminal_writer(config, *args, **kwargs): + """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 = py.io.TerminalWriter(*args, **kwargs) + if config.option.color == "yes": + tw.hasmarkup = True + if config.option.color == "no": + tw.hasmarkup = False + return tw + + +def _strtobool(val): + """Convert a string representation of truth to true (1) or false (0). + + 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"): + return 1 + elif val in ("n", "no", "f", "false", "off", "0"): + return 0 + else: + raise ValueError("invalid truth value %r" % (val,)) diff --git a/contrib/python/pytest/py2/_pytest/config/argparsing.py b/contrib/python/pytest/py2/_pytest/config/argparsing.py index e7d9b224ed..37fb772db9 100644 --- a/contrib/python/pytest/py2/_pytest/config/argparsing.py +++ b/contrib/python/pytest/py2/_pytest/config/argparsing.py @@ -1,410 +1,410 @@ # -*- coding: utf-8 -*- -import argparse -import warnings - -import py -import six - +import argparse +import warnings + +import py +import six + from _pytest.config.exceptions import UsageError - -FILE_OR_DIR = "file_or_dir" - - -class Parser(object): - """ 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. - """ - + +FILE_OR_DIR = "file_or_dir" + + +class Parser(object): + """ 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. + """ + prog = None - def __init__(self, usage=None, processopt=None): - self._anonymous = OptionGroup("custom options", parser=self) - self._groups = [] - self._processopt = processopt - self._usage = usage - self._inidict = {} - self._ininames = [] - self.extra_info = {} - - def processoption(self, option): - if self._processopt: - if option.dest: - self._processopt(option) - - def getgroup(self, name, description="", after=None): - """ get (or create) a named option Group. - - :name: name of the option group. - :description: long description for --help output. - :after: name of other group, used for ordering --help output. - - The returned group object has an ``addoption`` method with the same - signature as :py:func:`parser.addoption - <_pytest.config.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 - - def addoption(self, *opts, **attrs): - """ register a command line option. - - :opts: option names, can be short or long options. - :attrs: same attributes which the ``add_option()`` function of the - `argparse library - <http://docs.python.org/2/library/argparse.html>`_ - 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) - - def parse(self, args, namespace=None): - from _pytest._argcomplete import try_argcomplete - - self.optparser = self._getparser() - try_argcomplete(self.optparser) - args = [str(x) if isinstance(x, py.path.local) else x for x in args] - return self.optparser.parse_args(args, namespace=namespace) - - def _getparser(self): - from _pytest._argcomplete import filescompleter - + def __init__(self, usage=None, processopt=None): + self._anonymous = OptionGroup("custom options", parser=self) + self._groups = [] + self._processopt = processopt + self._usage = usage + self._inidict = {} + self._ininames = [] + self.extra_info = {} + + def processoption(self, option): + if self._processopt: + if option.dest: + self._processopt(option) + + def getgroup(self, name, description="", after=None): + """ get (or create) a named option Group. + + :name: name of the option group. + :description: long description for --help output. + :after: name of other group, used for ordering --help output. + + The returned group object has an ``addoption`` method with the same + signature as :py:func:`parser.addoption + <_pytest.config.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 + + def addoption(self, *opts, **attrs): + """ register a command line option. + + :opts: option names, can be short or long options. + :attrs: same attributes which the ``add_option()`` function of the + `argparse library + <http://docs.python.org/2/library/argparse.html>`_ + 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) + + def parse(self, args, namespace=None): + from _pytest._argcomplete import try_argcomplete + + self.optparser = self._getparser() + try_argcomplete(self.optparser) + args = [str(x) if isinstance(x, py.path.local) else x for x in args] + return self.optparser.parse_args(args, namespace=namespace) + + def _getparser(self): + 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) - # bash like autocompletion for dirs (appending '/') - optparser.add_argument(FILE_OR_DIR, nargs="*").completer = filescompleter - return optparser - - def parse_setoption(self, args, option, namespace=None): - parsedoption = self.parse(args, namespace=namespace) - for name, value in parsedoption.__dict__.items(): - setattr(option, name, value) - return getattr(parsedoption, FILE_OR_DIR) - - def parse_known_args(self, args, namespace=None): - """parses and returns a namespace object with known arguments at this - point. - """ - return self.parse_known_and_unknown_args(args, namespace=namespace)[0] - - def parse_known_and_unknown_args(self, args, namespace=None): - """parses and returns a namespace object with known arguments, and - the remaining arguments unknown at this point. - """ - optparser = self._getparser() - args = [str(x) if isinstance(x, py.path.local) else x for x in args] - return optparser.parse_known_args(args, namespace=namespace) - - def addini(self, name, help, type=None, default=None): - """ register an ini-file option. - - :name: name of the ini-variable - :type: type of the variable, can be ``pathlist``, ``args``, ``linelist`` - or ``bool``. - :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>`. - """ - assert type in (None, "pathlist", "args", "linelist", "bool") - 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, option): - self.msg = msg - self.option_id = str(option) - - def __str__(self): - if self.option_id: - return "option %s: %s" % (self.option_id, self.msg) - else: - return self.msg - - -class Argument(object): - """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} - - def __init__(self, *names, **attrs): - """store parms in private vars for use in add_argument""" - self._attrs = attrs - self._short_opts = [] - self._long_opts = [] - self.dest = attrs.get("dest") - 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, six.string_types): - 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: - # attribute existence is tested in Config._processopt - self.default = attrs["default"] - except KeyError: - pass - self._set_opt_strings(names) - if not self.dest: - if self._long_opts: - self.dest = self._long_opts[0][2:].replace("-", "_") - else: - try: - self.dest = self._short_opts[0][1:] - except IndexError: - raise ArgumentError("need a long or short option", self) - - def names(self): - return self._short_opts + self._long_opts - - def attrs(self): - # update any attributes set by processopt - attrs = "default dest help".split() - if self.dest: - 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 - - def _set_opt_strings(self, opts): - """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) - - def __repr__(self): - 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(object): - def __init__(self, name, description="", parser=None): - self.name = name - self.description = description - self.options = [] - self.parser = parser - - def addoption(self, *optnames, **attrs): - """ 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 - 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) - - def _addoption(self, *optnames, **attrs): - option = Argument(*optnames, **attrs) - self._addoption_instance(option, shortupper=True) - - def _addoption_instance(self, option, shortupper=False): - 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): + 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) + # bash like autocompletion for dirs (appending '/') + optparser.add_argument(FILE_OR_DIR, nargs="*").completer = filescompleter + return optparser + + def parse_setoption(self, args, option, namespace=None): + parsedoption = self.parse(args, namespace=namespace) + for name, value in parsedoption.__dict__.items(): + setattr(option, name, value) + return getattr(parsedoption, FILE_OR_DIR) + + def parse_known_args(self, args, namespace=None): + """parses and returns a namespace object with known arguments at this + point. + """ + return self.parse_known_and_unknown_args(args, namespace=namespace)[0] + + def parse_known_and_unknown_args(self, args, namespace=None): + """parses and returns a namespace object with known arguments, and + the remaining arguments unknown at this point. + """ + optparser = self._getparser() + args = [str(x) if isinstance(x, py.path.local) else x for x in args] + return optparser.parse_known_args(args, namespace=namespace) + + def addini(self, name, help, type=None, default=None): + """ register an ini-file option. + + :name: name of the ini-variable + :type: type of the variable, can be ``pathlist``, ``args``, ``linelist`` + or ``bool``. + :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>`. + """ + assert type in (None, "pathlist", "args", "linelist", "bool") + 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, option): + self.msg = msg + self.option_id = str(option) + + def __str__(self): + if self.option_id: + return "option %s: %s" % (self.option_id, self.msg) + else: + return self.msg + + +class Argument(object): + """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} + + def __init__(self, *names, **attrs): + """store parms in private vars for use in add_argument""" + self._attrs = attrs + self._short_opts = [] + self._long_opts = [] + self.dest = attrs.get("dest") + 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, six.string_types): + 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: + # attribute existence is tested in Config._processopt + self.default = attrs["default"] + except KeyError: + pass + self._set_opt_strings(names) + if not self.dest: + if self._long_opts: + self.dest = self._long_opts[0][2:].replace("-", "_") + else: + try: + self.dest = self._short_opts[0][1:] + except IndexError: + raise ArgumentError("need a long or short option", self) + + def names(self): + return self._short_opts + self._long_opts + + def attrs(self): + # update any attributes set by processopt + attrs = "default dest help".split() + if self.dest: + 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 + + def _set_opt_strings(self, opts): + """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) + + def __repr__(self): + 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(object): + def __init__(self, name, description="", parser=None): + self.name = name + self.description = description + self.options = [] + self.parser = parser + + def addoption(self, *optnames, **attrs): + """ 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 + 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) + + def _addoption(self, *optnames, **attrs): + option = Argument(*optnames, **attrs) + self._addoption_instance(option, shortupper=True) + + def _addoption_instance(self, option, shortupper=False): + 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, extra_info=None, prog=None): - if not extra_info: - extra_info = {} - self._parser = parser - argparse.ArgumentParser.__init__( - self, + if not extra_info: + extra_info = {} + self._parser = parser + argparse.ArgumentParser.__init__( + self, prog=prog, - usage=parser._usage, - add_help=False, - formatter_class=DropShorterLongHelpFormatter, - ) - # 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 - - def error(self, message): + usage=parser._usage, + add_help=False, + formatter_class=DropShorterLongHelpFormatter, + ) + # 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 + + def error(self, message): """Transform argparse error message into UsageError.""" msg = "%s: error: %s" % (self.prog, message) - + if hasattr(self._parser, "_config_source_hint"): msg = "%s (%s)" % (msg, self._parser._config_source_hint) - + raise UsageError(self.format_usage() + msg) - def parse_args(self, args=None, namespace=None): - """allow splitting of positional arguments""" - args, argv = self.parse_known_args(args, namespace) - if argv: - for arg in argv: - if arg and arg[0] == "-": - lines = ["unrecognized arguments: %s" % (" ".join(argv))] - for k, v in sorted(self.extra_info.items()): - lines.append(" %s: %s" % (k, v)) - self.error("\n".join(lines)) - getattr(args, FILE_OR_DIR).extend(argv) - return args - - -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 - - special action attribute map_long_option allows surpressing additional - long options - - shortcut if there are only two options and one of them is a short one - - cache result on action object as this is called at least 2 times - """ - - def _format_action_invocation(self, action): - orgstr = argparse.HelpFormatter._format_action_invocation(self, action) - if orgstr and orgstr[0] != "-": # only optional arguments - return orgstr - res = 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' - action._formatted_action_invocation = orgstr - return orgstr - return_list = [] - option_map = getattr(action, "map_long_option", {}) - if option_map is None: - option_map = {} - short_long = {} - for option in options: - if len(option) == 2 or option[2] == " ": - continue - if not option.startswith("--"): - raise ArgumentError( - 'long optional argument without "--": [%s]' % (option), self - ) - xxoption = option[2:] - if xxoption.split()[0] not in option_map: - 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)) - action._formatted_action_invocation = ", ".join(return_list) - return action._formatted_action_invocation + def parse_args(self, args=None, namespace=None): + """allow splitting of positional arguments""" + args, argv = self.parse_known_args(args, namespace) + if argv: + for arg in argv: + if arg and arg[0] == "-": + lines = ["unrecognized arguments: %s" % (" ".join(argv))] + for k, v in sorted(self.extra_info.items()): + lines.append(" %s: %s" % (k, v)) + self.error("\n".join(lines)) + getattr(args, FILE_OR_DIR).extend(argv) + return args + + +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 + - special action attribute map_long_option allows surpressing additional + long options + - shortcut if there are only two options and one of them is a short one + - cache result on action object as this is called at least 2 times + """ + + def _format_action_invocation(self, action): + orgstr = argparse.HelpFormatter._format_action_invocation(self, action) + if orgstr and orgstr[0] != "-": # only optional arguments + return orgstr + res = 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' + action._formatted_action_invocation = orgstr + return orgstr + return_list = [] + option_map = getattr(action, "map_long_option", {}) + if option_map is None: + option_map = {} + short_long = {} + for option in options: + if len(option) == 2 or option[2] == " ": + continue + if not option.startswith("--"): + raise ArgumentError( + 'long optional argument without "--": [%s]' % (option), self + ) + xxoption = option[2:] + if xxoption.split()[0] not in option_map: + 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)) + action._formatted_action_invocation = ", ".join(return_list) + return action._formatted_action_invocation diff --git a/contrib/python/pytest/py2/_pytest/config/exceptions.py b/contrib/python/pytest/py2/_pytest/config/exceptions.py index 3463aa2bd1..bf58fde5db 100644 --- a/contrib/python/pytest/py2/_pytest/config/exceptions.py +++ b/contrib/python/pytest/py2/_pytest/config/exceptions.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -class UsageError(Exception): - """ error in pytest usage or invocation""" - - -class PrintHelp(Exception): - """Raised when pytest should print it's help to skip the rest of the - argument parsing and validation.""" - - pass +class UsageError(Exception): + """ error in pytest usage or invocation""" + + +class PrintHelp(Exception): + """Raised when pytest should print it's help to skip the rest of the + argument parsing and validation.""" + + pass diff --git a/contrib/python/pytest/py2/_pytest/config/findpaths.py b/contrib/python/pytest/py2/_pytest/config/findpaths.py index f3776f1e75..e6779b289b 100644 --- a/contrib/python/pytest/py2/_pytest/config/findpaths.py +++ b/contrib/python/pytest/py2/_pytest/config/findpaths.py @@ -1,48 +1,48 @@ # -*- coding: utf-8 -*- -import os - -import py - -from .exceptions import UsageError +import os + +import py + +from .exceptions import UsageError from _pytest.outcomes import fail - - -def exists(path, ignore=EnvironmentError): - try: - return path.check() - except ignore: - return False - - -def getcfg(args, config=None): - """ - Search the list of arguments for a valid ini-file for pytest, - and return a tuple of (rootdir, inifile, cfg-dict). - - note: config is optional and used only to issue warnings explicitly (#2891). - """ - from _pytest.deprecated import CFG_PYTEST_SECTION - - inibasenames = ["pytest.ini", "tox.ini", "setup.cfg"] - args = [x for x in args if not str(x).startswith("-")] - if not args: - args = [py.path.local()] - for arg in args: - arg = py.path.local(arg) - for base in arg.parts(reverse=True): - for inibasename in inibasenames: - p = base.join(inibasename) - if exists(p): + + +def exists(path, ignore=EnvironmentError): + try: + return path.check() + except ignore: + return False + + +def getcfg(args, config=None): + """ + Search the list of arguments for a valid ini-file for pytest, + and return a tuple of (rootdir, inifile, cfg-dict). + + note: config is optional and used only to issue warnings explicitly (#2891). + """ + from _pytest.deprecated import CFG_PYTEST_SECTION + + inibasenames = ["pytest.ini", "tox.ini", "setup.cfg"] + args = [x for x in args if not str(x).startswith("-")] + if not args: + args = [py.path.local()] + for arg in args: + arg = py.path.local(arg) + for base in arg.parts(reverse=True): + for inibasename in inibasenames: + p = base.join(inibasename) + if exists(p): try: iniconfig = py.iniconfig.IniConfig(p) except py.iniconfig.ParseError as exc: raise UsageError(str(exc)) - - if ( - inibasename == "setup.cfg" - and "tool:pytest" in iniconfig.sections - ): - return base, p, iniconfig["tool:pytest"] + + if ( + inibasename == "setup.cfg" + and "tool:pytest" in iniconfig.sections + ): + return base, p, iniconfig["tool:pytest"] elif "pytest" in iniconfig.sections: if inibasename == "setup.cfg" and config is not None: @@ -51,103 +51,103 @@ def getcfg(args, config=None): pytrace=False, ) return base, p, iniconfig["pytest"] - elif inibasename == "pytest.ini": - # allowed to be empty - return base, p, {} - return None, None, None - - -def get_common_ancestor(paths): - common_ancestor = None - for path in paths: - if not path.exists(): - continue - if common_ancestor is None: - common_ancestor = path - else: - if path.relto(common_ancestor) or path == common_ancestor: - continue - elif common_ancestor.relto(path): - common_ancestor = path - else: - shared = path.common(common_ancestor) - if shared is not None: - common_ancestor = shared - if common_ancestor is None: - common_ancestor = py.path.local() - elif common_ancestor.isfile(): - common_ancestor = common_ancestor.dirpath() - return common_ancestor - - -def get_dirs_from_args(args): - def is_option(x): - return str(x).startswith("-") - - def get_file_part_from_node_id(x): - return str(x).split("::")[0] - - def get_dir_from_path(path): - if path.isdir(): - return path - return py.path.local(path.dirname) - - # These look like paths but may not exist - possible_paths = ( - py.path.local(get_file_part_from_node_id(arg)) - for arg in args - if not is_option(arg) - ) - - return [get_dir_from_path(path) for path in possible_paths if path.exists()] - - -def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): - dirs = get_dirs_from_args(args) - if inifile: - iniconfig = py.iniconfig.IniConfig(inifile) - is_cfg_file = str(inifile).endswith(".cfg") - sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"] - for section in sections: - try: - inicfg = iniconfig[section] - if is_cfg_file and section == "pytest" and config is not None: - from _pytest.deprecated import CFG_PYTEST_SECTION - + elif inibasename == "pytest.ini": + # allowed to be empty + return base, p, {} + return None, None, None + + +def get_common_ancestor(paths): + common_ancestor = None + for path in paths: + if not path.exists(): + continue + if common_ancestor is None: + common_ancestor = path + else: + if path.relto(common_ancestor) or path == common_ancestor: + continue + elif common_ancestor.relto(path): + common_ancestor = path + else: + shared = path.common(common_ancestor) + if shared is not None: + common_ancestor = shared + if common_ancestor is None: + common_ancestor = py.path.local() + elif common_ancestor.isfile(): + common_ancestor = common_ancestor.dirpath() + return common_ancestor + + +def get_dirs_from_args(args): + def is_option(x): + return str(x).startswith("-") + + def get_file_part_from_node_id(x): + return str(x).split("::")[0] + + def get_dir_from_path(path): + if path.isdir(): + return path + return py.path.local(path.dirname) + + # These look like paths but may not exist + possible_paths = ( + py.path.local(get_file_part_from_node_id(arg)) + for arg in args + if not is_option(arg) + ) + + return [get_dir_from_path(path) for path in possible_paths if path.exists()] + + +def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): + dirs = get_dirs_from_args(args) + if inifile: + iniconfig = py.iniconfig.IniConfig(inifile) + is_cfg_file = str(inifile).endswith(".cfg") + sections = ["tool:pytest", "pytest"] if is_cfg_file else ["pytest"] + for section in sections: + try: + inicfg = iniconfig[section] + if is_cfg_file and section == "pytest" and config is not None: + from _pytest.deprecated import CFG_PYTEST_SECTION + fail( CFG_PYTEST_SECTION.format(filename=str(inifile)), pytrace=False - ) - break - except KeyError: - inicfg = None + ) + break + except KeyError: + inicfg = None if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) - else: - ancestor = get_common_ancestor(dirs) - rootdir, inifile, inicfg = getcfg([ancestor], config=config) + else: + ancestor = get_common_ancestor(dirs) + rootdir, inifile, inicfg = getcfg([ancestor], config=config) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in ancestor.parts(reverse=True): if possible_rootdir.join("setup.py").exists(): rootdir = possible_rootdir - break - else: + break + else: if dirs != [ancestor]: rootdir, inifile, inicfg = getcfg(dirs, config=config) - if rootdir is None: + if rootdir is None: if config is not None: cwd = config.invocation_dir else: cwd = py.path.local() 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 = py.path.local(os.path.expandvars(rootdir_cmd_arg)) if not rootdir.isdir(): - raise UsageError( - "Directory '{}' not found. Check your '--rootdir' option.".format( + raise UsageError( + "Directory '{}' not found. Check your '--rootdir' option.".format( rootdir - ) - ) - return rootdir, inifile, inicfg or {} + ) + ) + return rootdir, inifile, inicfg or {} diff --git a/contrib/python/pytest/py2/_pytest/debugging.py b/contrib/python/pytest/py2/_pytest/debugging.py index 337b48d2f6..bc114c8683 100644 --- a/contrib/python/pytest/py2/_pytest/debugging.py +++ b/contrib/python/pytest/py2/_pytest/debugging.py @@ -1,56 +1,56 @@ # -*- coding: utf-8 -*- -""" interactive debugging with PDB, the Python Debugger. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os +""" interactive debugging with PDB, the Python Debugger. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os import argparse -import pdb -import sys -from doctest import UnexpectedException - -from _pytest import outcomes -from _pytest.config import hookimpl +import pdb +import sys +from doctest import UnexpectedException + +from _pytest import outcomes +from _pytest.config import hookimpl from _pytest.config.exceptions import UsageError - - -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): """Validate syntax of --pdbcls option.""" try: @@ -62,65 +62,65 @@ def _validate_usepdb_cls(value): return (modname, classname) -def pytest_addoption(parser): - 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", +def pytest_addoption(parser): + 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.", - ) - - -def pytest_configure(config): - if config.getvalue("trace"): - config.pluginmanager.register(PdbTrace(), "pdbtrace") - if config.getvalue("usepdb"): - config.pluginmanager.register(PdbInvoke(), "pdbinvoke") - - pytestPDB._saved.append( + 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): + 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). - def fin(): - ( - pdb.set_trace, - pytestPDB._pluginmanager, - pytestPDB._config, - ) = pytestPDB._saved.pop() - - config._cleanup.append(fin) - - -class pytestPDB(object): - """ Pseudo PDB that defers to the real pdb. """ - - _pluginmanager = None - _config = None - _saved = [] + ) + 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(): + ( + pdb.set_trace, + pytestPDB._pluginmanager, + pytestPDB._config, + ) = pytestPDB._saved.pop() + + config._cleanup.append(fin) + + +class pytestPDB(object): + """ Pseudo PDB that defers to the real pdb. """ + + _pluginmanager = None + _config = None + _saved = [] _recursive_debug = 0 _wrapped_pdb_cls = None - - @classmethod + + @classmethod def _is_capturing(cls, capman): if capman: return capman.is_capturing() @@ -163,18 +163,18 @@ class pytestPDB(object): @classmethod def _get_pdb_wrapper_class(cls, pdb_cls, capman): - import _pytest.config - + import _pytest.config + class PytestPdbWrapper(pdb_cls, object): _pytest_capman = capman _continued = False - + def do_debug(self, arg): cls._recursive_debug += 1 ret = super(PytestPdbWrapper, self).do_debug(arg) cls._recursive_debug -= 1 return ret - + def do_continue(self, arg): ret = super(PytestPdbWrapper, self).do_continue(arg) if cls._recursive_debug == 0: @@ -185,8 +185,8 @@ class pytestPDB(object): 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)" @@ -198,18 +198,18 @@ class pytestPDB(object): 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(PytestPdbWrapper, self).do_quit(arg) - + if cls._recursive_debug == 0: outcomes.exit("Quitting debugger") @@ -250,15 +250,15 @@ class pytestPDB(object): if cls._pluginmanager is not None: capman = cls._pluginmanager.getplugin("capturemanager") - else: + else: capman = None 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) @@ -276,7 +276,7 @@ class pytestPDB(object): ) else: tw.sep(">", "PDB %s" % (method,)) - + _pdb = cls._import_pdb_cls(capman)(**kwargs) if cls._pluginmanager: @@ -292,32 +292,32 @@ class pytestPDB(object): _pdb.set_trace(frame) -class PdbInvoke(object): - def pytest_exception_interact(self, node, call, report): - 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() - _enter_pdb(node, call.excinfo, report) - - def pytest_internalerror(self, excrepr, excinfo): - tb = _postmortem_traceback(excinfo) - post_mortem(tb) - - -class PdbTrace(object): - @hookimpl(hookwrapper=True) - def pytest_pyfunc_call(self, pyfuncitem): - _test_pytest_function(pyfuncitem) - yield - - -def _test_pytest_function(pyfuncitem): +class PdbInvoke(object): + def pytest_exception_interact(self, node, call, report): + 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() + _enter_pdb(node, call.excinfo, report) + + def pytest_internalerror(self, excrepr, excinfo): + tb = _postmortem_traceback(excinfo) + post_mortem(tb) + + +class PdbTrace(object): + @hookimpl(hookwrapper=True) + def pytest_pyfunc_call(self, pyfuncitem): + _test_pytest_function(pyfuncitem) + yield + + +def _test_pytest_function(pyfuncitem): _pdb = pytestPDB._init_pdb("runcall") - testfunction = pyfuncitem.obj + testfunction = pyfuncitem.obj pyfuncitem.obj = _pdb.runcall if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch raise ValueError("--trace can't be used with a fixture named func!") @@ -325,49 +325,49 @@ def _test_pytest_function(pyfuncitem): new_list = list(pyfuncitem._fixtureinfo.argnames) new_list.append("func") pyfuncitem._fixtureinfo.argnames = tuple(new_list) - - -def _enter_pdb(node, excinfo, rep): - # 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 + + +def _enter_pdb(node, excinfo, rep): + # 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 post_mortem(tb) - return rep - - -def _postmortem_traceback(excinfo): - 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] - else: - return excinfo._excinfo[2] - - -def post_mortem(t): + return rep + + +def _postmortem_traceback(excinfo): + 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] + else: + return excinfo._excinfo[2] + + +def post_mortem(t): 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/py2/_pytest/deprecated.py b/contrib/python/pytest/py2/_pytest/deprecated.py index c7b9b9d046..12394aca3f 100644 --- a/contrib/python/pytest/py2/_pytest/deprecated.py +++ b/contrib/python/pytest/py2/_pytest/deprecated.py @@ -1,63 +1,63 @@ # -*- coding: utf-8 -*- -""" -This module contains 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. - -All constants defined in this module should be either PytestWarning instances or UnformattedWarning -in case of warnings which need to format their messages. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warning_types import RemovedInPytest4Warning -from _pytest.warning_types import UnformattedWarning - +""" +This module contains 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. + +All constants defined in this module should be either PytestWarning instances or UnformattedWarning +in case of warnings which need to format their messages. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from _pytest.warning_types import PytestDeprecationWarning +from _pytest.warning_types import RemovedInPytest4Warning +from _pytest.warning_types import UnformattedWarning + YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" - - + + FIXTURE_FUNCTION_CALL = ( 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' "but are created automatically when test functions request them as parameters.\n" "See https://docs.pytest.org/en/latest/fixture.html for more information about fixtures, and\n" "https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly about how to update your code." -) - -FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( - "'request' is a reserved name for fixtures and will raise an error in future versions" -) - +) + +FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( + "'request' is a reserved name for fixtures and will raise an error in future versions" +) + CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." - -GETFUNCARGVALUE = RemovedInPytest4Warning( - "getfuncargvalue is deprecated, use getfixturevalue" -) - + +GETFUNCARGVALUE = RemovedInPytest4Warning( + "getfuncargvalue is deprecated, use getfixturevalue" +) + RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning( "The 'message' parameter is deprecated.\n" "(did you mean to use `match='some regex'` to check the exception message?)\n" "Please see:\n" " https://docs.pytest.org/en/4.6-maintenance/deprecations.html#message-parameter-of-pytest-raises" -) - +) + RESULT_LOG = PytestDeprecationWarning( "--result-log is deprecated and scheduled for removal in pytest 5.0.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." -) - +) + RAISES_EXEC = PytestDeprecationWarning( "raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n" "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" -) +) WARNS_EXEC = PytestDeprecationWarning( "warns(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly.\n\n" "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" -) - +) + PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported " "because it affects the entire directory tree in a non-explicit way.\n" @@ -66,31 +66,31 @@ PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( " {}\n" "For more information, visit:\n" " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" -) - +) + PYTEST_CONFIG_GLOBAL = PytestDeprecationWarning( "the `pytest.config` global is deprecated. Please use `request.config` " "or `pytest_configure` (if you're a pytest plugin) instead." -) - +) + PYTEST_ENSURETEMP = RemovedInPytest4Warning( "pytest/tmpdir_factory.ensuretemp is deprecated, \n" "please use the tmp_path fixture or tmp_path_factory.mktemp" -) - +) + PYTEST_LOGWARNING = PytestDeprecationWarning( "pytest_logwarning is deprecated, no longer being called, and will be removed soon\n" "please use pytest_warning_captured instead" -) - +) + PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning( PytestDeprecationWarning, "pytest.warns() got unexpected keyword arguments: {args!r}.\n" "This will be an error in future versions.", -) - +) + PYTEST_PARAM_UNKNOWN_KWARGS = UnformattedWarning( PytestDeprecationWarning, "pytest.param() got unexpected keyword arguments: {args!r}.\n" "This will be an error in future versions.", -) +) diff --git a/contrib/python/pytest/py2/_pytest/doctest.py b/contrib/python/pytest/py2/_pytest/doctest.py index eaedb7bb13..659d24aeeb 100644 --- a/contrib/python/pytest/py2/_pytest/doctest.py +++ b/contrib/python/pytest/py2/_pytest/doctest.py @@ -1,359 +1,359 @@ # -*- coding: utf-8 -*- -""" discover and run doctests in modules and test files.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - +""" discover and run doctests in modules and test files.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + import inspect -import platform -import sys -import traceback +import platform +import sys +import traceback import warnings from contextlib import contextmanager - -import pytest -from _pytest._code.code import ExceptionInfo -from _pytest._code.code import ReprFileLocation -from _pytest._code.code import TerminalRepr + +import pytest +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ReprFileLocation +from _pytest._code.code import TerminalRepr from _pytest.compat import safe_getattr -from _pytest.fixtures import FixtureRequest +from _pytest.fixtures import FixtureRequest from _pytest.outcomes import Skipped from _pytest.warning_types import PytestWarning - -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 - - -def pytest_addoption(parser): - 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_collect_file(path, parent): - config = parent.config - if path.ext == ".py": - if config.option.doctestmodules and not _is_setup_py(config, path, parent): - return DoctestModule(path, parent) - elif _is_doctest(config, path, parent): - return DoctestTextfile(path, parent) - - -def _is_setup_py(config, path, parent): - if path.basename != "setup.py": - return False - contents = path.read() - return "setuptools" in contents or "distutils" in contents - - -def _is_doctest(config, path, parent): - 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): - # List of (reprlocation, lines) tuples - self.reprlocation_lines = reprlocation_lines - - def toterminal(self, tw): - for reprlocation, lines in self.reprlocation_lines: - for line in lines: - tw.line(line) - reprlocation.toterminal(tw) - - -class MultipleDoctestFailures(Exception): - def __init__(self, failures): - super(MultipleDoctestFailures, self).__init__() - self.failures = failures - - -def _init_runner_class(): - 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__( - self, checker=None, verbose=None, optionflags=0, continue_on_failure=True - ): - doctest.DebugRunner.__init__( - self, checker=checker, verbose=verbose, optionflags=optionflags - ) - self.continue_on_failure = continue_on_failure - - def report_failure(self, out, test, example, got): - failure = doctest.DocTestFailure(test, example, got) - if self.continue_on_failure: - out.append(failure) - else: - raise failure - - def report_unexpected_exception(self, out, test, example, exc_info): + +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 + + +def pytest_addoption(parser): + 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_collect_file(path, parent): + config = parent.config + if path.ext == ".py": + if config.option.doctestmodules and not _is_setup_py(config, path, parent): + return DoctestModule(path, parent) + elif _is_doctest(config, path, parent): + return DoctestTextfile(path, parent) + + +def _is_setup_py(config, path, parent): + if path.basename != "setup.py": + return False + contents = path.read() + return "setuptools" in contents or "distutils" in contents + + +def _is_doctest(config, path, parent): + 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): + # List of (reprlocation, lines) tuples + self.reprlocation_lines = reprlocation_lines + + def toterminal(self, tw): + for reprlocation, lines in self.reprlocation_lines: + for line in lines: + tw.line(line) + reprlocation.toterminal(tw) + + +class MultipleDoctestFailures(Exception): + def __init__(self, failures): + super(MultipleDoctestFailures, self).__init__() + self.failures = failures + + +def _init_runner_class(): + 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__( + self, checker=None, verbose=None, optionflags=0, continue_on_failure=True + ): + doctest.DebugRunner.__init__( + self, checker=checker, verbose=verbose, optionflags=optionflags + ) + self.continue_on_failure = continue_on_failure + + def report_failure(self, out, test, example, got): + failure = doctest.DocTestFailure(test, example, got) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + def report_unexpected_exception(self, out, test, example, exc_info): if isinstance(exc_info[1], Skipped): raise exc_info[1] - failure = doctest.UnexpectedException(test, example, exc_info) - if self.continue_on_failure: - out.append(failure) - else: - raise failure - - return PytestDoctestRunner - - -def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=True): - # 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() - return RUNNER_CLASS( - checker=checker, - verbose=verbose, - optionflags=optionflags, - continue_on_failure=continue_on_failure, - ) - - -class DoctestItem(pytest.Item): - def __init__(self, name, parent, runner=None, dtest=None): - super(DoctestItem, self).__init__(name, parent) - self.runner = runner - self.dtest = dtest - self.obj = None - self.fixture_request = None - - def setup(self): - 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): - _check_all_skipped(self.dtest) - self._disable_output_capturing_for_darwin() - failures = [] - self.runner.run(self.dtest, out=failures) - if failures: - raise MultipleDoctestFailures(failures) - - def _disable_output_capturing_for_darwin(self): - """ - 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) - - def repr_failure(self, excinfo): - import doctest - - failures = None - if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)): - failures = [excinfo.value] - elif excinfo.errisinstance(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__ - reprlocation = ReprFileLocation(filename, lineno, message) - checker = _get_checker() - report_choice = _get_report_choice( - self.config.getoption("doctestreport") - ) - if lineno is not None: - lines = failure.test.docstring.splitlines(False) - # add line numbers to the left of the error message - 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("??? %s %s" % (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)] - lines += traceback.format_exception(*failure.exc_info) - reprlocation_lines.append((reprlocation, lines)) - return ReprFailDoctest(reprlocation_lines) - else: - return super(DoctestItem, self).repr_failure(excinfo) - - def reportinfo(self): - return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name - - -def _get_flag_lookup(): - 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(), - ) - - -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 - - def collect(self): - 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( - verbose=0, - optionflags=optionflags, - checker=_get_checker(), - continue_on_failure=_get_continue_on_failure(self.config), - ) - _fix_spoof_python2(runner, encoding) - - parser = doctest.DocTestParser() - test = parser.get_doctest(text, globs, name, filename, 0) - if test.examples: - yield DoctestItem(test.name, self, runner, test) - - -def _check_all_skipped(test): - """raises 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") - - + failure = doctest.UnexpectedException(test, example, exc_info) + if self.continue_on_failure: + out.append(failure) + else: + raise failure + + return PytestDoctestRunner + + +def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=True): + # 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() + return RUNNER_CLASS( + checker=checker, + verbose=verbose, + optionflags=optionflags, + continue_on_failure=continue_on_failure, + ) + + +class DoctestItem(pytest.Item): + def __init__(self, name, parent, runner=None, dtest=None): + super(DoctestItem, self).__init__(name, parent) + self.runner = runner + self.dtest = dtest + self.obj = None + self.fixture_request = None + + def setup(self): + 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): + _check_all_skipped(self.dtest) + self._disable_output_capturing_for_darwin() + failures = [] + self.runner.run(self.dtest, out=failures) + if failures: + raise MultipleDoctestFailures(failures) + + def _disable_output_capturing_for_darwin(self): + """ + 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) + + def repr_failure(self, excinfo): + import doctest + + failures = None + if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)): + failures = [excinfo.value] + elif excinfo.errisinstance(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__ + reprlocation = ReprFileLocation(filename, lineno, message) + checker = _get_checker() + report_choice = _get_report_choice( + self.config.getoption("doctestreport") + ) + if lineno is not None: + lines = failure.test.docstring.splitlines(False) + # add line numbers to the left of the error message + 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("??? %s %s" % (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)] + lines += traceback.format_exception(*failure.exc_info) + reprlocation_lines.append((reprlocation, lines)) + return ReprFailDoctest(reprlocation_lines) + else: + return super(DoctestItem, self).repr_failure(excinfo) + + def reportinfo(self): + return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name + + +def _get_flag_lookup(): + 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(), + ) + + +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 + + def collect(self): + 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( + verbose=0, + optionflags=optionflags, + checker=_get_checker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + _fix_spoof_python2(runner, encoding) + + parser = doctest.DocTestParser() + test = parser.get_doctest(text, globs, name, filename, 0) + if test.examples: + yield DoctestItem(test.name, self, runner, test) + + +def _check_all_skipped(test): + """raises 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") + + def _is_mocked(obj): """ returns if a object is possibly a mock object by checking the existence of a highly improbable attribute @@ -396,10 +396,10 @@ def _patch_unwrap_mock_aware(): inspect.unwrap = real_unwrap -class DoctestModule(pytest.Module): - def collect(self): - import doctest - +class DoctestModule(pytest.Module): + def collect(self): + import doctest + class MockAwareDocTestFinder(doctest.DocTestFinder): """ a hackish doctest finder that overrides stdlib internals to fix a stdlib bug @@ -417,167 +417,167 @@ class DoctestModule(pytest.Module): self, tests, obj, name, module, source_lines, globs, seen ) - if self.fspath.basename == "conftest.py": - module = self.config.pluginmanager._importconftest(self.fspath) - else: - try: - module = self.fspath.pyimport() - 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 + if self.fspath.basename == "conftest.py": + module = self.config.pluginmanager._importconftest(self.fspath) + else: + try: + module = self.fspath.pyimport() + 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( - verbose=0, - 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(test.name, self, runner, test) - - -def _setup_fixtures(doctest_item): - """ - Used by DoctestTextfile and DoctestItem to setup fixture information. - """ - - def func(): - pass - - doctest_item.funcargs = {} - fm = doctest_item.session._fixturemanager - doctest_item._fixtureinfo = fm.getfixtureinfo( - node=doctest_item, func=func, cls=None, funcargs=False - ) - fixture_request = FixtureRequest(doctest_item) - fixture_request._fillfixtures() - return fixture_request - - -def _get_checker(): - """ - Returns a doctest.OutputChecker subclass that takes in account the - ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES - to strip b'' prefixes. - Useful when the same doctest should run in Python 2 and Python 3. - - An inner class is used to avoid importing "doctest" at the module - level. - """ - if hasattr(_get_checker, "LiteralsOutputChecker"): - return _get_checker.LiteralsOutputChecker() - - import doctest - import re - - class LiteralsOutputChecker(doctest.OutputChecker): - """ - Copied from doctest_nose_plugin.py from the nltk project: - https://github.com/nltk/nltk - - Further extended to also support byte literals. - """ - - _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) - _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) - - def check_output(self, want, got, optionflags): - res = doctest.OutputChecker.check_output(self, want, got, optionflags) - if res: - return True - - allow_unicode = optionflags & _get_allow_unicode_flag() - allow_bytes = optionflags & _get_allow_bytes_flag() - if not allow_unicode and not allow_bytes: - return False - - else: # pragma: no cover - - def remove_prefixes(regex, txt): - 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) - res = doctest.OutputChecker.check_output(self, want, got, optionflags) - return res - - _get_checker.LiteralsOutputChecker = LiteralsOutputChecker - return _get_checker.LiteralsOutputChecker() - - -def _get_allow_unicode_flag(): - """ - Registers and returns the ALLOW_UNICODE flag. - """ - import doctest - - return doctest.register_optionflag("ALLOW_UNICODE") - - -def _get_allow_bytes_flag(): - """ - Registers and returns the ALLOW_BYTES flag. - """ - import doctest - - return doctest.register_optionflag("ALLOW_BYTES") - - -def _get_report_choice(key): - """ - This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid - importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests. - """ - 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] - - -def _fix_spoof_python2(runner, encoding): - """ - Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This - should patch only doctests for text files because they don't have a way to declare their - encoding. Doctests in docstrings from Python modules don't have the same problem given that - Python already decoded the strings. - - This fixes the problem related in issue #2434. - """ - from _pytest.compat import _PY2 - - if not _PY2: - return - - from doctest import _SpoofOut - - class UnicodeSpoof(_SpoofOut): - def getvalue(self): - result = _SpoofOut.getvalue(self) - if encoding and isinstance(result, bytes): - result = result.decode(encoding) - return result - - runner._fakeout = UnicodeSpoof() - - -@pytest.fixture(scope="session") -def doctest_namespace(): - """ - Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. - """ - return dict() + optionflags = get_optionflags(self) + runner = _get_runner( + verbose=0, + 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(test.name, self, runner, test) + + +def _setup_fixtures(doctest_item): + """ + Used by DoctestTextfile and DoctestItem to setup fixture information. + """ + + def func(): + pass + + doctest_item.funcargs = {} + fm = doctest_item.session._fixturemanager + doctest_item._fixtureinfo = fm.getfixtureinfo( + node=doctest_item, func=func, cls=None, funcargs=False + ) + fixture_request = FixtureRequest(doctest_item) + fixture_request._fillfixtures() + return fixture_request + + +def _get_checker(): + """ + Returns a doctest.OutputChecker subclass that takes in account the + ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES + to strip b'' prefixes. + Useful when the same doctest should run in Python 2 and Python 3. + + An inner class is used to avoid importing "doctest" at the module + level. + """ + if hasattr(_get_checker, "LiteralsOutputChecker"): + return _get_checker.LiteralsOutputChecker() + + import doctest + import re + + class LiteralsOutputChecker(doctest.OutputChecker): + """ + Copied from doctest_nose_plugin.py from the nltk project: + https://github.com/nltk/nltk + + Further extended to also support byte literals. + """ + + _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) + _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) + + def check_output(self, want, got, optionflags): + res = doctest.OutputChecker.check_output(self, want, got, optionflags) + if res: + return True + + allow_unicode = optionflags & _get_allow_unicode_flag() + allow_bytes = optionflags & _get_allow_bytes_flag() + if not allow_unicode and not allow_bytes: + return False + + else: # pragma: no cover + + def remove_prefixes(regex, txt): + 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) + res = doctest.OutputChecker.check_output(self, want, got, optionflags) + return res + + _get_checker.LiteralsOutputChecker = LiteralsOutputChecker + return _get_checker.LiteralsOutputChecker() + + +def _get_allow_unicode_flag(): + """ + Registers and returns the ALLOW_UNICODE flag. + """ + import doctest + + return doctest.register_optionflag("ALLOW_UNICODE") + + +def _get_allow_bytes_flag(): + """ + Registers and returns the ALLOW_BYTES flag. + """ + import doctest + + return doctest.register_optionflag("ALLOW_BYTES") + + +def _get_report_choice(key): + """ + This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid + importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests. + """ + 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] + + +def _fix_spoof_python2(runner, encoding): + """ + Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This + should patch only doctests for text files because they don't have a way to declare their + encoding. Doctests in docstrings from Python modules don't have the same problem given that + Python already decoded the strings. + + This fixes the problem related in issue #2434. + """ + from _pytest.compat import _PY2 + + if not _PY2: + return + + from doctest import _SpoofOut + + class UnicodeSpoof(_SpoofOut): + def getvalue(self): + result = _SpoofOut.getvalue(self) + if encoding and isinstance(result, bytes): + result = result.decode(encoding) + return result + + runner._fakeout = UnicodeSpoof() + + +@pytest.fixture(scope="session") +def doctest_namespace(): + """ + Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. + """ + return dict() diff --git a/contrib/python/pytest/py2/_pytest/fixtures.py b/contrib/python/pytest/py2/_pytest/fixtures.py index bbf88a44fc..280a48608b 100644 --- a/contrib/python/pytest/py2/_pytest/fixtures.py +++ b/contrib/python/pytest/py2/_pytest/fixtures.py @@ -1,667 +1,667 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import functools -import inspect +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import functools +import inspect import itertools -import sys -import warnings -from collections import defaultdict -from collections import deque -from collections import OrderedDict - -import attr -import py -import six - -import _pytest -from _pytest import nodes +import sys +import warnings +from collections import defaultdict +from collections import deque +from collections import OrderedDict + +import attr +import py +import six + +import _pytest +from _pytest import nodes from _pytest._code.code import FormattedExcinfo -from _pytest._code.code import TerminalRepr -from _pytest.compat import _format_args -from _pytest.compat import _PytestWrapper -from _pytest.compat import exc_clear -from _pytest.compat import FuncargnamesCompatAttr -from _pytest.compat import get_real_func -from _pytest.compat import get_real_method -from _pytest.compat import getfslineno -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 isclass -from _pytest.compat import NOTSET -from _pytest.compat import safe_getattr -from _pytest.deprecated import FIXTURE_FUNCTION_CALL -from _pytest.deprecated import FIXTURE_NAMED_REQUEST -from _pytest.outcomes import fail -from _pytest.outcomes import TEST_OUTCOME - - -@attr.s(frozen=True) -class PseudoFixtureDef(object): - cached_result = attr.ib() - scope = attr.ib() - - -def pytest_sessionstart(session): - import _pytest.python - import _pytest.nodes - - scopename2class.update( - { - "package": _pytest.python.Package, - "class": _pytest.python.Class, - "module": _pytest.python.Module, - "function": _pytest.nodes.Item, - "session": _pytest.main.Session, - } - ) - session._fixturemanager = FixtureManager(session) - - -scopename2class = {} - - -scope2props = dict(session=()) -scope2props["package"] = ("fspath",) -scope2props["module"] = ("fspath", "module") -scope2props["class"] = scope2props["module"] + ("cls",) -scope2props["instance"] = scope2props["class"] + ("instance",) -scope2props["function"] = scope2props["instance"] + ("function", "keywords") - - -def scopeproperty(name=None, doc=None): - def decoratescope(func): - scopename = name or func.__name__ - - def provide(self): - if func.__name__ in scope2props[self.scope]: - return func(self) - raise AttributeError( - "%s not available in %s-scoped context" % (scopename, self.scope) - ) - - return property(provide, None, None, func.__doc__) - - return decoratescope - - -def get_scope_package(node, fixturedef): - import pytest - - cls = pytest.Package - current = node - fixture_package_name = "%s/%s" % (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 - - -def get_scope_node(node, scope): - cls = scopename2class.get(scope) - if cls is None: - raise ValueError("unknown scope") - return node.getparent(cls) - - -def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): - # this function will transform all collected calls to a 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: - return # this function call does not have direct parametrization - # collect funcargs of all callspecs into a list of values - arg2params = {} - arg2scope = {} - 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(): - # 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) - # use module-level collector for class-scope (for now) - node = collector - if node and argname in node._name2pseudofixturedef: - arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] - else: - fixturedef = FixtureDef( - fixturemanager, - "", - argname, - get_direct_param_fixture_func, - arg2scope[argname], - valuelist, - False, - False, - ) - arg2fixturedefs[argname] = [fixturedef] - if node is not None: - node._name2pseudofixturedef[argname] = fixturedef - - -def getfixturemarker(obj): - """ return fixturemarker or None if it doesn't exist or raised - exceptions.""" - try: - return 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 - - -def get_parametrized_fixture_keys(item, scopenum): - """ return list of keys for all parametrized arguments which match - the specified scope. """ - assert scopenum < scopenum_function # function - try: - cs = item.callspec - except AttributeError: - pass - else: - # 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 = (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 - key = (argname, param_index, item.fspath, item.cls) - 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" -# setups and teardowns - - -def reorder_items(items): - argkeys_cache = {} - items_by_argkey = {} - for scopenum in range(0, scopenum_function): - argkeys_cache[scopenum] = d = {} - items_by_argkey[scopenum] = item_d = defaultdict(deque) - for item in items: - keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum)) - if keys: - d[item] = keys - for key in keys: - item_d[key].append(item) - items = OrderedDict.fromkeys(items) - return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0)) - - -def fix_cache_order(item, argkeys_cache, items_by_argkey): - 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, argkeys_cache, items_by_argkey, scopenum): - if scopenum >= scopenum_function or len(items) < 3: - return items - ignore = set() - items_deque = deque(items) - items_done = OrderedDict() - scoped_items_by_argkey = items_by_argkey[scopenum] - scoped_argkeys_cache = argkeys_cache[scopenum] - while items_deque: - no_argkey_group = OrderedDict() - slicing_argkey = None - while items_deque: - item = items_deque.popleft() - if item in items_done or item in no_argkey_group: - continue - argkeys = OrderedDict.fromkeys( - k for k in scoped_argkeys_cache.get(item, []) if k not in ignore - ) - 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 - - -def fillfixtures(function): - """ fill missing funcargs for a test function. """ - 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 - fi = fm.getfixtureinfo(function.parent, function.obj, None) - function._fixtureinfo = fi - request = function._request = FixtureRequest(function) - 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) -class FuncFixtureInfo(object): - # original function argument names - argnames = attr.ib(type=tuple) - # argnames that function immediately requires. These include argnames + - # fixture names specified via usefixtures and via autouse=True in fixture - # definitions. - initialnames = attr.ib(type=tuple) +from _pytest._code.code import TerminalRepr +from _pytest.compat import _format_args +from _pytest.compat import _PytestWrapper +from _pytest.compat import exc_clear +from _pytest.compat import FuncargnamesCompatAttr +from _pytest.compat import get_real_func +from _pytest.compat import get_real_method +from _pytest.compat import getfslineno +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 isclass +from _pytest.compat import NOTSET +from _pytest.compat import safe_getattr +from _pytest.deprecated import FIXTURE_FUNCTION_CALL +from _pytest.deprecated import FIXTURE_NAMED_REQUEST +from _pytest.outcomes import fail +from _pytest.outcomes import TEST_OUTCOME + + +@attr.s(frozen=True) +class PseudoFixtureDef(object): + cached_result = attr.ib() + scope = attr.ib() + + +def pytest_sessionstart(session): + import _pytest.python + import _pytest.nodes + + scopename2class.update( + { + "package": _pytest.python.Package, + "class": _pytest.python.Class, + "module": _pytest.python.Module, + "function": _pytest.nodes.Item, + "session": _pytest.main.Session, + } + ) + session._fixturemanager = FixtureManager(session) + + +scopename2class = {} + + +scope2props = dict(session=()) +scope2props["package"] = ("fspath",) +scope2props["module"] = ("fspath", "module") +scope2props["class"] = scope2props["module"] + ("cls",) +scope2props["instance"] = scope2props["class"] + ("instance",) +scope2props["function"] = scope2props["instance"] + ("function", "keywords") + + +def scopeproperty(name=None, doc=None): + def decoratescope(func): + scopename = name or func.__name__ + + def provide(self): + if func.__name__ in scope2props[self.scope]: + return func(self) + raise AttributeError( + "%s not available in %s-scoped context" % (scopename, self.scope) + ) + + return property(provide, None, None, func.__doc__) + + return decoratescope + + +def get_scope_package(node, fixturedef): + import pytest + + cls = pytest.Package + current = node + fixture_package_name = "%s/%s" % (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 + + +def get_scope_node(node, scope): + cls = scopename2class.get(scope) + if cls is None: + raise ValueError("unknown scope") + return node.getparent(cls) + + +def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager): + # this function will transform all collected calls to a 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: + return # this function call does not have direct parametrization + # collect funcargs of all callspecs into a list of values + arg2params = {} + arg2scope = {} + 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(): + # 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) + # use module-level collector for class-scope (for now) + node = collector + if node and argname in node._name2pseudofixturedef: + arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]] + else: + fixturedef = FixtureDef( + fixturemanager, + "", + argname, + get_direct_param_fixture_func, + arg2scope[argname], + valuelist, + False, + False, + ) + arg2fixturedefs[argname] = [fixturedef] + if node is not None: + node._name2pseudofixturedef[argname] = fixturedef + + +def getfixturemarker(obj): + """ return fixturemarker or None if it doesn't exist or raised + exceptions.""" + try: + return 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 + + +def get_parametrized_fixture_keys(item, scopenum): + """ return list of keys for all parametrized arguments which match + the specified scope. """ + assert scopenum < scopenum_function # function + try: + cs = item.callspec + except AttributeError: + pass + else: + # 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 = (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 + key = (argname, param_index, item.fspath, item.cls) + 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" +# setups and teardowns + + +def reorder_items(items): + argkeys_cache = {} + items_by_argkey = {} + for scopenum in range(0, scopenum_function): + argkeys_cache[scopenum] = d = {} + items_by_argkey[scopenum] = item_d = defaultdict(deque) + for item in items: + keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum)) + if keys: + d[item] = keys + for key in keys: + item_d[key].append(item) + items = OrderedDict.fromkeys(items) + return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0)) + + +def fix_cache_order(item, argkeys_cache, items_by_argkey): + 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, argkeys_cache, items_by_argkey, scopenum): + if scopenum >= scopenum_function or len(items) < 3: + return items + ignore = set() + items_deque = deque(items) + items_done = OrderedDict() + scoped_items_by_argkey = items_by_argkey[scopenum] + scoped_argkeys_cache = argkeys_cache[scopenum] + while items_deque: + no_argkey_group = OrderedDict() + slicing_argkey = None + while items_deque: + item = items_deque.popleft() + if item in items_done or item in no_argkey_group: + continue + argkeys = OrderedDict.fromkeys( + k for k in scoped_argkeys_cache.get(item, []) if k not in ignore + ) + 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 + + +def fillfixtures(function): + """ fill missing funcargs for a test function. """ + 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 + fi = fm.getfixtureinfo(function.parent, function.obj, None) + function._fixtureinfo = fi + request = function._request = FixtureRequest(function) + 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) +class FuncFixtureInfo(object): + # original function argument names + argnames = attr.ib(type=tuple) + # argnames that function immediately requires. These include argnames + + # fixture names specified via usefixtures and via autouse=True in fixture + # definitions. + initialnames = attr.ib(type=tuple) names_closure = attr.ib() # List[str] name2fixturedefs = attr.ib() # List[str, List[FixtureDef]] - - def prune_dependency_tree(self): - """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. - """ - closure = set() - 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) - - -class FixtureRequest(FuncargnamesCompatAttr): - """ 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): - self._pyfuncitem = pyfuncitem - #: fixture for which this request is being performed - self.fixturename = None - #: Scope string, one of "function", "class", "module", "session" - self.scope = "function" - self._fixture_defs = {} # argname -> FixtureDef - fixtureinfo = pyfuncitem._fixtureinfo - self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() - self._arg2index = {} - self._fixturemanager = pyfuncitem.session._fixturemanager - - @property - def fixturenames(self): - """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): - """ underlying collection node (depends on current request scope)""" - return self._getscopeitem(self.scope) - - def _getnextfixturedef(self, argname): - 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 - # not known at parsing/collection time - parentid = self._pyfuncitem.parent.nodeid - fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) - self._arg2fixturedefs[argname] = fixturedefs - # 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 - def config(self): - """ the pytest config object associated with this request. """ - return self._pyfuncitem.config - - @scopeproperty() - def function(self): - """ test function object if the request has a per-function scope. """ - return self._pyfuncitem.obj - - @scopeproperty("class") - def cls(self): - """ class (can be None) where the test function was collected. """ - 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) - - @scopeproperty() - def module(self): - """ python module object where the test function was collected. """ - return self._pyfuncitem.getparent(_pytest.python.Module).obj - - @scopeproperty() - def fspath(self): - """ the file system path of the test module which collected this test. """ - return self._pyfuncitem.fspath - - @property - def keywords(self): - """ keywords/markers dictionary for the underlying node. """ - return self.node.keywords - - @property - def session(self): - """ pytest session object. """ - return self._pyfuncitem.session - - def addfinalizer(self, finalizer): - """ 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) - - def _addfinalizer(self, finalizer, scope): - colitem = self._getscopeitem(scope) - self._pyfuncitem.session._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem - ) - - def applymarker(self, marker): - """ 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. - - :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object - created by a call to ``pytest.mark.NAME(...)``. - """ - self.node.add_marker(marker) - - def raiseerror(self, msg): - """ raise a FixtureLookupError with the given message. """ - raise self._fixturemanager.FixtureLookupError(None, self, msg) - - def _fillfixtures(self): - 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): - """ 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. - """ - return self._get_active_fixturedef(argname).cached_result[0] - - def getfuncargvalue(self, argname): - """ Deprecated, use getfixturevalue. """ - from _pytest import deprecated - - warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2) - return self.getfixturevalue(argname) - - def _get_active_fixturedef(self, argname): - try: - return self._fixture_defs[argname] - except KeyError: - try: - fixturedef = self._getnextfixturedef(argname) - except FixtureLookupError: - if argname == "request": - cached_result = (self, [0], None) - scope = "function" - 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 - - def _get_fixturestack(self): - current = self - values = [] - while 1: - fixturedef = getattr(current, "_fixturedef", None) - if fixturedef is None: - values.reverse() - return values - values.append(fixturedef) - current = current._parent_request - - def _compute_fixture_value(self, fixturedef): - """ - Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. 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. - - :param FixtureDef fixturedef: - """ - # 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 = frameinfo.filename - source_lineno = frameinfo.lineno - source_path = py.path.local(source_path) - if source_path.relto(funcitem.config.rootdir): - source_path = source_path.relto(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, - source_lineno, - ) - ) - fail(msg, pytrace=False) - else: + + def prune_dependency_tree(self): + """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. + """ + closure = set() + 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) + + +class FixtureRequest(FuncargnamesCompatAttr): + """ 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): + self._pyfuncitem = pyfuncitem + #: fixture for which this request is being performed + self.fixturename = None + #: Scope string, one of "function", "class", "module", "session" + self.scope = "function" + self._fixture_defs = {} # argname -> FixtureDef + fixtureinfo = pyfuncitem._fixtureinfo + self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() + self._arg2index = {} + self._fixturemanager = pyfuncitem.session._fixturemanager + + @property + def fixturenames(self): + """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): + """ underlying collection node (depends on current request scope)""" + return self._getscopeitem(self.scope) + + def _getnextfixturedef(self, argname): + 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 + # not known at parsing/collection time + parentid = self._pyfuncitem.parent.nodeid + fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) + self._arg2fixturedefs[argname] = fixturedefs + # 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 + def config(self): + """ the pytest config object associated with this request. """ + return self._pyfuncitem.config + + @scopeproperty() + def function(self): + """ test function object if the request has a per-function scope. """ + return self._pyfuncitem.obj + + @scopeproperty("class") + def cls(self): + """ class (can be None) where the test function was collected. """ + 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) + + @scopeproperty() + def module(self): + """ python module object where the test function was collected. """ + return self._pyfuncitem.getparent(_pytest.python.Module).obj + + @scopeproperty() + def fspath(self): + """ the file system path of the test module which collected this test. """ + return self._pyfuncitem.fspath + + @property + def keywords(self): + """ keywords/markers dictionary for the underlying node. """ + return self.node.keywords + + @property + def session(self): + """ pytest session object. """ + return self._pyfuncitem.session + + def addfinalizer(self, finalizer): + """ 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) + + def _addfinalizer(self, finalizer, scope): + colitem = self._getscopeitem(scope) + self._pyfuncitem.session._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem + ) + + def applymarker(self, marker): + """ 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. + + :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object + created by a call to ``pytest.mark.NAME(...)``. + """ + self.node.add_marker(marker) + + def raiseerror(self, msg): + """ raise a FixtureLookupError with the given message. """ + raise self._fixturemanager.FixtureLookupError(None, self, msg) + + def _fillfixtures(self): + 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): + """ 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. + """ + return self._get_active_fixturedef(argname).cached_result[0] + + def getfuncargvalue(self, argname): + """ Deprecated, use getfixturevalue. """ + from _pytest import deprecated + + warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2) + return self.getfixturevalue(argname) + + def _get_active_fixturedef(self, argname): + try: + return self._fixture_defs[argname] + except KeyError: + try: + fixturedef = self._getnextfixturedef(argname) + except FixtureLookupError: + if argname == "request": + cached_result = (self, [0], None) + scope = "function" + 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 + + def _get_fixturestack(self): + current = self + values = [] + while 1: + fixturedef = getattr(current, "_fixturedef", None) + if fixturedef is None: + values.reverse() + return values + values.append(fixturedef) + current = current._parent_request + + def _compute_fixture_value(self, fixturedef): + """ + Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. 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. + + :param FixtureDef fixturedef: + """ + # 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 = frameinfo.filename + source_lineno = frameinfo.lineno + source_path = py.path.local(source_path) + if source_path.relto(funcitem.config.rootdir): + source_path = source_path.relto(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, + 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] - - subrequest = SubRequest(self, scope, param, param_index, fixturedef) - - # check if a higher-level scoped fixture accesses a lower level one - subrequest._check_scope(argname, self.scope, scope) - - # clear sys.exc_info before invoking the fixture (python bug?) - # if it's not explicitly cleared it will leak into the call - exc_clear() - try: - # call the fixture function - fixturedef.execute(request=subrequest) - finally: + # 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] + + subrequest = SubRequest(self, scope, param, param_index, fixturedef) + + # check if a higher-level scoped fixture accesses a lower level one + subrequest._check_scope(argname, self.scope, scope) + + # clear sys.exc_info before invoking the fixture (python bug?) + # if it's not explicitly cleared it will leak into the call + exc_clear() + try: + # call the fixture function + fixturedef.execute(request=subrequest) + finally: self._schedule_finalizers(fixturedef, subrequest) - + def _schedule_finalizers(self, fixturedef, subrequest): # if fixture function failed it might have registered finalizers self.session._setupstate.addfinalizer( functools.partial(fixturedef.finish, request=subrequest), subrequest.node ) - def _check_scope(self, argname, 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, - ) - - def _factorytraceback(self): - 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) + def _check_scope(self, argname, 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, + ) + + def _factorytraceback(self): + 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 - - def _getscopeitem(self, scope): - if scope == "function": - # this might also be a non-function Item despite its attribute name - return self._pyfuncitem - if scope == "package": - node = get_scope_package(self._pyfuncitem, self._fixturedef) - 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 - - def __repr__(self): - return "<FixtureRequest for %r>" % (self.node) - - -class SubRequest(FixtureRequest): - """ a sub request for handling getting a fixture from a - test function/fixture. """ - - def __init__(self, request, scope, param, param_index, fixturedef): - 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): - return "<SubRequest %r for %r>" % (self.fixturename, self._pyfuncitem) - - def addfinalizer(self, finalizer): - self._fixturedef.addfinalizer(finalizer) - + return lines + + def _getscopeitem(self, scope): + if scope == "function": + # this might also be a non-function Item despite its attribute name + return self._pyfuncitem + if scope == "package": + node = get_scope_package(self._pyfuncitem, self._fixturedef) + 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 + + def __repr__(self): + return "<FixtureRequest for %r>" % (self.node) + + +class SubRequest(FixtureRequest): + """ a sub request for handling getting a fixture from a + test function/fixture. """ + + def __init__(self, request, scope, param, param_index, fixturedef): + 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): + return "<SubRequest %r for %r>" % (self.fixturename, self._pyfuncitem) + + def addfinalizer(self, finalizer): + self._fixturedef.addfinalizer(finalizer) + def _schedule_finalizers(self, fixturedef, subrequest): # if the executing fixturedef was not explicitly requested in the argument list (via # getfixturevalue inside the fixture call) then ensure this fixture def will be finished @@ -671,409 +671,409 @@ class SubRequest(FixtureRequest): functools.partial(self._fixturedef.finish, request=self) ) super(SubRequest, self)._schedule_finalizers(fixturedef, subrequest) - - -scopes = "session package module class function".split() -scopenum_function = scopes.index("function") - - -def scopemismatch(currentscope, newscope): - return scopes.index(newscope) > scopes.index(currentscope) - - -def scope2index(scope, descr, where=None): - """Look up the index of ``scope`` and raise a descriptive value error - if not defined. - """ - try: - return scopes.index(scope) - except ValueError: - fail( - "{} {}got an unexpected scope value '{}'".format( - descr, "from {} ".format(where) if where else "", scope - ), - pytrace=False, - ) - - -class FixtureLookupError(LookupError): - """ could not return a requested Fixture (missing or invalid). """ - - def __init__(self, argname, request, msg=None): - self.argname = argname - self.request = request - self.fixturestack = request._get_fixturestack() - self.msg = msg - - def formatrepr(self): - tblines = [] - 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)) - except (IOError, IndexError, TypeError): - error_msg = "file %s, line %s: source code not available" - addline(error_msg % (fspath, lineno + 1)) - else: - addline("file %s, line %s" % (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: - msg = "fixture '{}' not found".format(self.argname) - 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, firstlineno, tblines, errorstring, argname): - self.tblines = tblines - self.errorstring = errorstring - self.filename = filename - self.firstlineno = firstlineno - self.argname = argname - - def toterminal(self, tw): - # 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( - "{} {}".format(FormattedExcinfo.flow_marker, line.strip()), - red=True, - ) - tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) - - -def fail_fixturefunc(fixturefunc, msg): - fs, lineno = getfslineno(fixturefunc) - location = "%s:%s" % (fs, lineno + 1) - source = _pytest._code.Source(fixturefunc) - fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) - - -def call_fixture_func(fixturefunc, request, kwargs): - yieldctx = is_generator(fixturefunc) - if yieldctx: - it = fixturefunc(**kwargs) - res = next(it) - finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, it) - request.addfinalizer(finalizer) - else: - res = fixturefunc(**kwargs) - return res - - -def _teardown_yield_fixture(fixturefunc, it): - """Executes 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: - fail_fixturefunc( - fixturefunc, "yield_fixture function has more than one 'yield'" - ) - - -class FixtureDef(object): - """ A container for a factory definition. """ - - def __init__( - self, - fixturemanager, - baseid, - argname, - func, - scope, - params, - unittest=False, - ids=None, - ): - self._fixturemanager = fixturemanager - self.baseid = baseid or "" - self.has_location = baseid is not None - self.func = func - self.argname = argname - self.scope = scope - self.scopenum = scope2index( - scope or "function", - descr="Fixture '{}'".format(func.__name__), - where=baseid, - ) - self.params = params - self.argnames = getfuncargnames(func, is_method=unittest) - self.unittest = unittest - self.ids = ids - self._finalizers = [] - - def addfinalizer(self, finalizer): - self._finalizers.append(finalizer) - - def finish(self, request): - exceptions = [] - try: - while self._finalizers: - try: - func = self._finalizers.pop() - func() - except: # noqa - exceptions.append(sys.exc_info()) - if exceptions: - e = exceptions[0] + + +scopes = "session package module class function".split() +scopenum_function = scopes.index("function") + + +def scopemismatch(currentscope, newscope): + return scopes.index(newscope) > scopes.index(currentscope) + + +def scope2index(scope, descr, where=None): + """Look up the index of ``scope`` and raise a descriptive value error + if not defined. + """ + try: + return scopes.index(scope) + except ValueError: + fail( + "{} {}got an unexpected scope value '{}'".format( + descr, "from {} ".format(where) if where else "", scope + ), + pytrace=False, + ) + + +class FixtureLookupError(LookupError): + """ could not return a requested Fixture (missing or invalid). """ + + def __init__(self, argname, request, msg=None): + self.argname = argname + self.request = request + self.fixturestack = request._get_fixturestack() + self.msg = msg + + def formatrepr(self): + tblines = [] + 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)) + except (IOError, IndexError, TypeError): + error_msg = "file %s, line %s: source code not available" + addline(error_msg % (fspath, lineno + 1)) + else: + addline("file %s, line %s" % (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: + msg = "fixture '{}' not found".format(self.argname) + 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, firstlineno, tblines, errorstring, argname): + self.tblines = tblines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + self.argname = argname + + def toterminal(self, tw): + # 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( + "{} {}".format(FormattedExcinfo.flow_marker, line.strip()), + red=True, + ) + tw.line() + tw.line("%s:%d" % (self.filename, self.firstlineno + 1)) + + +def fail_fixturefunc(fixturefunc, msg): + fs, lineno = getfslineno(fixturefunc) + location = "%s:%s" % (fs, lineno + 1) + source = _pytest._code.Source(fixturefunc) + fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False) + + +def call_fixture_func(fixturefunc, request, kwargs): + yieldctx = is_generator(fixturefunc) + if yieldctx: + it = fixturefunc(**kwargs) + res = next(it) + finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, it) + request.addfinalizer(finalizer) + else: + res = fixturefunc(**kwargs) + return res + + +def _teardown_yield_fixture(fixturefunc, it): + """Executes 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: + fail_fixturefunc( + fixturefunc, "yield_fixture function has more than one 'yield'" + ) + + +class FixtureDef(object): + """ A container for a factory definition. """ + + def __init__( + self, + fixturemanager, + baseid, + argname, + func, + scope, + params, + unittest=False, + ids=None, + ): + self._fixturemanager = fixturemanager + self.baseid = baseid or "" + self.has_location = baseid is not None + self.func = func + self.argname = argname + self.scope = scope + self.scopenum = scope2index( + scope or "function", + descr="Fixture '{}'".format(func.__name__), + where=baseid, + ) + self.params = params + self.argnames = getfuncargnames(func, is_method=unittest) + self.unittest = unittest + self.ids = ids + self._finalizers = [] + + def addfinalizer(self, finalizer): + self._finalizers.append(finalizer) + + def finish(self, request): + exceptions = [] + try: + while self._finalizers: + try: + func = self._finalizers.pop() + func() + except: # noqa + exceptions.append(sys.exc_info()) + if exceptions: + e = exceptions[0] # Ensure to not keep frame references through traceback. del exceptions - six.reraise(*e) - 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 - if hasattr(self, "cached_result"): - del self.cached_result - self._finalizers = [] - - def execute(self, request): - # 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": - fixturedef.addfinalizer(functools.partial(self.finish, request=request)) - - my_cache_key = request.param_index - cached_result = getattr(self, "cached_result", None) - if cached_result is not None: - result, cache_key, err = cached_result - if my_cache_key == cache_key: - if err is not None: - six.reraise(*err) - else: - 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) - assert not hasattr(self, "cached_result") - - hook = self._fixturemanager.session.gethookproxy(request.node.fspath) - return hook.pytest_fixture_setup(fixturedef=self, request=request) - - def __repr__(self): - return "<FixtureDef argname=%r scope=%r baseid=%r>" % ( - self.argname, - self.scope, - self.baseid, - ) - - -def resolve_fixture_function(fixturedef, request): - """Gets 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: - # bind the unbound method to the TestCase instance - fixturefunc = fixturedef.func.__get__(request.instance) - 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: - fixturefunc = getimfunc(fixturedef.func) - if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(request.instance) - return fixturefunc - - -def pytest_fixture_setup(fixturedef, request): - """ Execution of fixture setup. """ - kwargs = {} - for argname in fixturedef.argnames: - fixdef = request._get_active_fixturedef(argname) - 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 = request.param_index - try: - result = call_fixture_func(fixturefunc, request, kwargs) - except TEST_OUTCOME: - fixturedef.cached_result = (None, my_cache_key, sys.exc_info()) - raise - fixturedef.cached_result = (result, my_cache_key, None) - return result - - -def _ensure_immutable_ids(ids): - if ids is None: - return - if callable(ids): - return ids - return tuple(ids) - - + six.reraise(*e) + 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 + if hasattr(self, "cached_result"): + del self.cached_result + self._finalizers = [] + + def execute(self, request): + # 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": + fixturedef.addfinalizer(functools.partial(self.finish, request=request)) + + my_cache_key = request.param_index + cached_result = getattr(self, "cached_result", None) + if cached_result is not None: + result, cache_key, err = cached_result + if my_cache_key == cache_key: + if err is not None: + six.reraise(*err) + else: + 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) + assert not hasattr(self, "cached_result") + + hook = self._fixturemanager.session.gethookproxy(request.node.fspath) + return hook.pytest_fixture_setup(fixturedef=self, request=request) + + def __repr__(self): + return "<FixtureDef argname=%r scope=%r baseid=%r>" % ( + self.argname, + self.scope, + self.baseid, + ) + + +def resolve_fixture_function(fixturedef, request): + """Gets 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: + # bind the unbound method to the TestCase instance + fixturefunc = fixturedef.func.__get__(request.instance) + 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: + fixturefunc = getimfunc(fixturedef.func) + if fixturefunc != fixturedef.func: + fixturefunc = fixturefunc.__get__(request.instance) + return fixturefunc + + +def pytest_fixture_setup(fixturedef, request): + """ Execution of fixture setup. """ + kwargs = {} + for argname in fixturedef.argnames: + fixdef = request._get_active_fixturedef(argname) + 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 = request.param_index + try: + result = call_fixture_func(fixturefunc, request, kwargs) + except TEST_OUTCOME: + fixturedef.cached_result = (None, my_cache_key, sys.exc_info()) + raise + fixturedef.cached_result = (result, my_cache_key, None) + return result + + +def _ensure_immutable_ids(ids): + if ids is None: + return + if callable(ids): + return ids + return tuple(ids) + + def wrap_function_to_error_out_if_called_directly(function, fixture_marker): """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function. - """ + """ message = FIXTURE_FUNCTION_CALL.format( - name=fixture_marker.name or function.__name__ - ) - + name=fixture_marker.name or function.__name__ + ) + @six.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) - - return result - - -@attr.s(frozen=True) -class FixtureFunctionMarker(object): - scope = attr.ib() - params = attr.ib(converter=attr.converters.optional(tuple)) - autouse = attr.ib(default=False) - ids = attr.ib(default=None, converter=_ensure_immutable_ids) - name = attr.ib(default=None) - - def __call__(self, function): - if 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" - ) - + + # 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) + + return result + + +@attr.s(frozen=True) +class FixtureFunctionMarker(object): + scope = attr.ib() + params = attr.ib(converter=attr.converters.optional(tuple)) + autouse = attr.ib(default=False) + ids = attr.ib(default=None, converter=_ensure_immutable_ids) + name = attr.ib(default=None) + + def __call__(self, function): + if 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" + ) + function = wrap_function_to_error_out_if_called_directly(function, self) - - name = self.name or function.__name__ - if name == "request": - warnings.warn(FIXTURE_NAMED_REQUEST) - function._pytestfixturefunction = self - return function - - -def fixture(scope="function", params=None, autouse=False, ids=None, name=None): - """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. - - :arg scope: the scope for which this fixture is shared, one of - ``"function"`` (default), ``"class"``, ``"module"``, - ``"package"`` or ``"session"``. - - ``"package"`` is considered **experimental** at this time. - - :arg params: an optional list of parameters which will cause multiple - invocations of the fixture function and all of the tests - using it. + + name = self.name or function.__name__ + if name == "request": + warnings.warn(FIXTURE_NAMED_REQUEST) + function._pytestfixturefunction = self + return function + + +def fixture(scope="function", params=None, autouse=False, ids=None, name=None): + """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. + + :arg scope: the scope for which this fixture is shared, one of + ``"function"`` (default), ``"class"``, ``"module"``, + ``"package"`` or ``"session"``. + + ``"package"`` is considered **experimental** at this time. + + :arg 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``. - - :arg autouse: if True, the fixture func is activated for all tests that - can see it. If False (the default) then an explicit - reference is needed to activate the fixture. - - :arg 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. - - :arg 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 defined, the function name of the fixture will be - shadowed by the 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>')``. - """ - if callable(scope) and params is None and autouse is False: - # direct decoration - return FixtureFunctionMarker("function", params, autouse, name=name)(scope) - if params is not None and not isinstance(params, (list, tuple)): - params = list(params) - return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) - - -def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=None): - """ (return a) decorator to mark a yield-fixture factory function. - - .. deprecated:: 3.0 - Use :py:func:`pytest.fixture` directly instead. - """ - return fixture(scope=scope, params=params, autouse=autouse, ids=ids, name=name) - - -defaultfuncargprefixmarker = fixture() - - -@fixture(scope="session") -def pytestconfig(request): - """Session-scoped fixture that returns the :class:`_pytest.config.Config` object. - - Example:: - - def test_foo(pytestconfig): + + :arg autouse: if True, the fixture func is activated for all tests that + can see it. If False (the default) then an explicit + reference is needed to activate the fixture. + + :arg 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. + + :arg 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 defined, the function name of the fixture will be + shadowed by the 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>')``. + """ + if callable(scope) and params is None and autouse is False: + # direct decoration + return FixtureFunctionMarker("function", params, autouse, name=name)(scope) + if params is not None and not isinstance(params, (list, tuple)): + params = list(params) + return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) + + +def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=None): + """ (return a) decorator to mark a yield-fixture factory function. + + .. deprecated:: 3.0 + Use :py:func:`pytest.fixture` directly instead. + """ + return fixture(scope=scope, params=params, autouse=autouse, ids=ids, name=name) + + +defaultfuncargprefixmarker = fixture() + + +@fixture(scope="session") +def pytestconfig(request): + """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.addini( "usefixtures", @@ -1083,50 +1083,50 @@ def pytest_addoption(parser): ) -class FixtureManager(object): - """ - pytest fixtures 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 - 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, - 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 - - def __init__(self, session): - self.session = session - self.config = session.config - self._arg2fixturedefs = {} - self._holderobjseen = set() - self._arg2finish = {} - self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] - session.config.pluginmanager.register(self, "funcmanage") - +class FixtureManager(object): + """ + pytest fixtures 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 + 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, + 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 + + def __init__(self, session): + self.session = session + self.config = session.config + self._arg2fixturedefs = {} + self._holderobjseen = set() + self._arg2finish = {} + self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))] + session.config.pluginmanager.register(self, "funcmanage") + def _get_direct_parametrize_args(self, node): """This function returns all the direct parametrization arguments of a node, so we don't mistake them for fixtures @@ -1148,106 +1148,106 @@ class FixtureManager(object): return parametrize_argnames - def getfixtureinfo(self, node, func, cls, funcargs=True): - if funcargs and not getattr(node, "nofuncargs", False): - argnames = getfuncargnames(func, cls=cls) - else: - argnames = () + def getfixtureinfo(self, node, func, cls, funcargs=True): + if funcargs and not getattr(node, "nofuncargs", False): + argnames = getfuncargnames(func, cls=cls) + else: + argnames = () usefixtures = itertools.chain.from_iterable( - mark.args for mark in node.iter_markers(name="usefixtures") - ) - initialnames = tuple(usefixtures) + argnames - fm = node.session._fixturemanager - initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( + mark.args for mark in node.iter_markers(name="usefixtures") + ) + initialnames = tuple(usefixtures) + argnames + 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) - - def pytest_plugin_registered(self, plugin): - nodeid = None - try: - p = py.path.local(plugin.__file__).realpath() - except AttributeError: - pass - else: - # construct the base nodeid which is later used to check - # what fixtures are visible for particular tests (as denoted - # by their test id) - if p.basename.startswith("conftest.py"): - nodeid = p.dirpath().relto(self.config.rootdir) - if p.sep != nodes.SEP: - nodeid = nodeid.replace(p.sep, nodes.SEP) - - self.parsefactories(plugin, nodeid) - - def _getautousenames(self, nodeid): - """ return a tuple of fixture names to be used. """ - autousenames = [] - for baseid, basenames in self._nodeid_and_autousenames: - if nodeid.startswith(baseid): - if baseid: - i = len(baseid) - nextchar = nodeid[i : i + 1] - if nextchar and nextchar not in ":/": - continue - autousenames.extend(basenames) - return autousenames - + ) + return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) + + def pytest_plugin_registered(self, plugin): + nodeid = None + try: + p = py.path.local(plugin.__file__).realpath() + except AttributeError: + pass + else: + # construct the base nodeid which is later used to check + # what fixtures are visible for particular tests (as denoted + # by their test id) + if p.basename.startswith("conftest.py"): + nodeid = p.dirpath().relto(self.config.rootdir) + if p.sep != nodes.SEP: + nodeid = nodeid.replace(p.sep, nodes.SEP) + + self.parsefactories(plugin, nodeid) + + def _getautousenames(self, nodeid): + """ return a tuple of fixture names to be used. """ + autousenames = [] + for baseid, basenames in self._nodeid_and_autousenames: + if nodeid.startswith(baseid): + if baseid: + i = len(baseid) + nextchar = nodeid[i : i + 1] + if nextchar and nextchar not in ":/": + continue + autousenames.extend(basenames) + return autousenames + def getfixtureclosure(self, fixturenames, parentnode, ignore_args=()): - # 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 - # (discovering matching fixtures for a given name/node is expensive) - - parentid = parentnode.nodeid - fixturenames_closure = self._getautousenames(parentid) - - def merge(otherlist): - 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) - - arg2fixturedefs = {} - lastlen = -1 - while lastlen != len(fixturenames_closure): - lastlen = len(fixturenames_closure) - for argname in fixturenames_closure: + # 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 + # (discovering matching fixtures for a given name/node is expensive) + + parentid = parentnode.nodeid + fixturenames_closure = self._getautousenames(parentid) + + def merge(otherlist): + 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) + + arg2fixturedefs = {} + 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) - - def sort_by_scope(arg_name): - 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): - for argname in metafunc.fixturenames: - faclist = metafunc._arg2fixturedefs.get(argname) - if faclist: - fixturedef = faclist[-1] - if fixturedef.params is not None: + 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): + 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): + for argname in metafunc.fixturenames: + faclist = metafunc._arg2fixturedefs.get(argname) + if faclist: + fixturedef = faclist[-1] + if fixturedef.params is not None: markers = list(metafunc.definition.iter_markers("parametrize")) for parametrize_mark in markers: if "argnames" in parametrize_mark.kwargs: @@ -1261,96 +1261,96 @@ class FixtureManager(object): ] if argname in argnames: break - else: - metafunc.parametrize( - argname, - fixturedef.params, - indirect=True, - scope=fixturedef.scope, - ids=fixturedef.ids, - ) - else: - continue # will raise FixtureLookupError at setup time - - def pytest_collection_modifyitems(self, items): - # separate parametrized setups - items[:] = reorder_items(items) - - def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): - 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) + else: + metafunc.parametrize( + argname, + fixturedef.params, + indirect=True, + scope=fixturedef.scope, + ids=fixturedef.ids, + ) + else: + continue # will raise FixtureLookupError at setup time + + def pytest_collection_modifyitems(self, items): + # separate parametrized setups + items[:] = reorder_items(items) + + def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False): + 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 - + # magic globals with __getattr__ might have got us a wrong + # fixture attribute + continue + if marker.name: name = marker.name - # during fixture definition we wrap the original fixture function - # to issue a warning if called directly, so here we unwrap it in order to not emit the warning - # when pytest itself calls the fixture function - if six.PY2 and unittest: - # hack on Python 2 because of the unbound methods - obj = get_real_func(obj) - else: - obj = get_real_method(obj, holderobj) - - fixture_def = FixtureDef( - self, - nodeid, - name, - obj, - marker.scope, - 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: - self._nodeid_and_autousenames.append((nodeid or "", autousenames)) - - def getfixturedefs(self, argname, nodeid): - """ - Gets a list of fixtures which are applicable to the given node id. - - :param str argname: name of the fixture to search for - :param str nodeid: full node id of the requesting test. - :return: list[FixtureDef] - """ - try: - fixturedefs = self._arg2fixturedefs[argname] - except KeyError: - return None - return tuple(self._matchfactories(fixturedefs, nodeid)) - - def _matchfactories(self, fixturedefs, nodeid): - for fixturedef in fixturedefs: - if nodes.ischildnode(fixturedef.baseid, nodeid): - yield fixturedef + # during fixture definition we wrap the original fixture function + # to issue a warning if called directly, so here we unwrap it in order to not emit the warning + # when pytest itself calls the fixture function + if six.PY2 and unittest: + # hack on Python 2 because of the unbound methods + obj = get_real_func(obj) + else: + obj = get_real_method(obj, holderobj) + + fixture_def = FixtureDef( + self, + nodeid, + name, + obj, + marker.scope, + 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: + self._nodeid_and_autousenames.append((nodeid or "", autousenames)) + + def getfixturedefs(self, argname, nodeid): + """ + Gets a list of fixtures which are applicable to the given node id. + + :param str argname: name of the fixture to search for + :param str nodeid: full node id of the requesting test. + :return: list[FixtureDef] + """ + try: + fixturedefs = self._arg2fixturedefs[argname] + except KeyError: + return None + return tuple(self._matchfactories(fixturedefs, nodeid)) + + def _matchfactories(self, fixturedefs, nodeid): + for fixturedef in fixturedefs: + if nodes.ischildnode(fixturedef.baseid, nodeid): + yield fixturedef diff --git a/contrib/python/pytest/py2/_pytest/freeze_support.py b/contrib/python/pytest/py2/_pytest/freeze_support.py index 1743e38bf6..aeeec2a56b 100644 --- a/contrib/python/pytest/py2/_pytest/freeze_support.py +++ b/contrib/python/pytest/py2/_pytest/freeze_support.py @@ -1,48 +1,48 @@ # -*- coding: utf-8 -*- -""" -Provides a function to report all internal modules for using freezing tools -pytest -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - - -def freeze_includes(): - """ - Returns 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 - - -def _iter_all_modules(package, prefix=""): - """ - Iterates over the names of all modules that can be found in the given - package, recursively. - Example: - _iter_all_modules(_pytest) -> - ['_pytest.assertion.newinterpret', - '_pytest.capture', - '_pytest.core', - ... - ] - """ - import os - import pkgutil - - if type(package) is not str: - path, prefix = package.__path__[0], package.__name__ + "." - else: - path = package - 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 +""" +Provides a function to report all internal modules for using freezing tools +pytest +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +def freeze_includes(): + """ + Returns 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 + + +def _iter_all_modules(package, prefix=""): + """ + Iterates over the names of all modules that can be found in the given + package, recursively. + Example: + _iter_all_modules(_pytest) -> + ['_pytest.assertion.newinterpret', + '_pytest.capture', + '_pytest.core', + ... + ] + """ + import os + import pkgutil + + if type(package) is not str: + path, prefix = package.__path__[0], package.__name__ + "." + else: + path = package + 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/py2/_pytest/helpconfig.py b/contrib/python/pytest/py2/_pytest/helpconfig.py index c886794733..5681160160 100644 --- a/contrib/python/pytest/py2/_pytest/helpconfig.py +++ b/contrib/python/pytest/py2/_pytest/helpconfig.py @@ -1,124 +1,124 @@ # -*- coding: utf-8 -*- -""" version info, help messages, tracing configuration. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os -import sys -from argparse import Action - -import py - -import pytest -from _pytest.config import PrintHelp - - -class HelpAction(Action): - """This is 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): - super(HelpAction, self).__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) - - # We should only skip the rest of the parsing after preparse is done - if getattr(parser._parser, "after_preparse", False): - raise PrintHelp - - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group.addoption( - "--version", - action="store_true", - help="display pytest lib version and import information.", - ) - 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", +""" version info, help messages, tracing configuration. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import sys +from argparse import Action + +import py + +import pytest +from _pytest.config import PrintHelp + + +class HelpAction(Action): + """This is 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): + super(HelpAction, self).__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) + + # We should only skip the rest of the parsing after preparse is done + if getattr(parser._parser, "after_preparse", False): + raise PrintHelp + + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group.addoption( + "--version", + action="store_true", + help="display pytest lib version and import information.", + ) + 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). " - "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`.', - ) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_cmdline_parse(): - outcome = yield - 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(), - config._origargs, - ) - ) - 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(): - 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) - - + "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`.', + ) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_cmdline_parse(): + outcome = yield + 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(), + config._origargs, + ) + ) + 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(): + 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): p = py.path.local(pytest.__file__) sys.stderr.write( @@ -130,36 +130,36 @@ def showversion(config): sys.stderr.write(line + "\n") -def pytest_cmdline_main(config): - if config.option.version: +def pytest_cmdline_main(config): + if config.option.version: showversion(config) - return 0 - elif config.option.help: - config._do_configure() - showhelp(config) - config._ensure_unconfigure() - return 0 - - -def showhelp(config): + return 0 + elif config.option.help: + config._do_configure() + showhelp(config) + config._ensure_unconfigure() + return 0 + + +def showhelp(config): 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" spec = "%s (%s):" % (name, type) tw.write(" %s" % spec) spec_len = len(spec) @@ -173,7 +173,7 @@ def showhelp(config): subsequent_indent=indent, break_on_hyphens=False, ) - + for line in helplines: tw.line(line) else: @@ -185,63 +185,63 @@ def showhelp(config): 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(" %-24s %s" % (name, 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")] - - -def getpluginversioninfo(config): - 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 = "%s-%s at %s" % (dist.project_name, dist.version, loc) - lines.append(" " + content) - return lines - - -def pytest_report_header(config): - lines = [] - if config.option.debug or config.option.traceconfig: - lines.append("using: pytest-%s pylib-%s" % (pytest.__version__, 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) - lines.append(" %-20s: %s" % (name, r)) - return lines + 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(" %-24s %s" % (name, 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")] + + +def getpluginversioninfo(config): + 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 = "%s-%s at %s" % (dist.project_name, dist.version, loc) + lines.append(" " + content) + return lines + + +def pytest_report_header(config): + lines = [] + if config.option.debug or config.option.traceconfig: + lines.append("using: pytest-%s pylib-%s" % (pytest.__version__, 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) + lines.append(" %-20s: %s" % (name, r)) + return lines diff --git a/contrib/python/pytest/py2/_pytest/hookspec.py b/contrib/python/pytest/py2/_pytest/hookspec.py index 7a78bbd3b1..7ab6154b17 100644 --- a/contrib/python/pytest/py2/_pytest/hookspec.py +++ b/contrib/python/pytest/py2/_pytest/hookspec.py @@ -1,385 +1,385 @@ # -*- coding: utf-8 -*- -""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ -from pluggy import HookspecMarker - +""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ +from pluggy import HookspecMarker + from _pytest.deprecated import PYTEST_LOGWARNING - -hookspec = HookspecMarker("pytest") - -# ------------------------------------------------------------------------- -# Initialization hooks called for every plugin -# ------------------------------------------------------------------------- - - -@hookspec(historic=True) -def pytest_addhooks(pluginmanager): - """called at plugin registration time to allow adding new hooks via a call to - ``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) -def pytest_plugin_registered(plugin, manager): - """ 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) -def pytest_addoption(parser): - """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>`. - - :arg _pytest.config.Parser parser: To add command line options, call - :py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`. - To add ini-file values call :py:func:`parser.addini(...) - <_pytest.config.Parser.addini>`. - - 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): - """ - Allows 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``. - - :arg _pytest.config.Config config: pytest config object - """ - - -# ------------------------------------------------------------------------- -# Bootstrapping hooks called for plugins registered early enough: -# internal and 3rd party plugins. -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) -def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. - - Stops at first non-None result, see :ref:`firstresult` - - .. note:: + +hookspec = HookspecMarker("pytest") + +# ------------------------------------------------------------------------- +# Initialization hooks called for every plugin +# ------------------------------------------------------------------------- + + +@hookspec(historic=True) +def pytest_addhooks(pluginmanager): + """called at plugin registration time to allow adding new hooks via a call to + ``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) +def pytest_plugin_registered(plugin, manager): + """ 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) +def pytest_addoption(parser): + """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>`. + + :arg _pytest.config.Parser parser: To add command line options, call + :py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`. + To add ini-file values call :py:func:`parser.addini(...) + <_pytest.config.Parser.addini>`. + + 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): + """ + Allows 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``. + + :arg _pytest.config.Config config: pytest config object + """ + + +# ------------------------------------------------------------------------- +# Bootstrapping hooks called for plugins registered early enough: +# internal and 3rd party plugins. +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_cmdline_parse(pluginmanager, args): + """return initialized config object, parsing the specified args. + + Stops at first non-None result, see :ref:`firstresult` + + .. 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, args): - """(**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: pytest config object - :param list[str] args: list of arguments passed on the command line - """ - - -@hookspec(firstresult=True) -def pytest_cmdline_main(config): - """ 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. - - Stops at first non-None result, see :ref:`firstresult` - - :param _pytest.config.Config config: pytest config object - """ - - -def pytest_load_initial_conftests(early_config, parser, args): - """ implements 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. - - :param _pytest.config.Config early_config: pytest config object - :param list[str] args: list of arguments passed on the command line - :param _pytest.config.Parser parser: to add command line options - """ - - -# ------------------------------------------------------------------------- -# collection hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) -def pytest_collection(session): - """Perform the collection protocol for the given session. - - Stops at first non-None result, see :ref:`firstresult`. - - :param _pytest.main.Session session: the pytest session object - """ - - -def pytest_collection_modifyitems(session, config, items): - """ called after collection has been performed, may filter or re-order - the items in-place. - - :param _pytest.main.Session session: the pytest session object - :param _pytest.config.Config config: pytest config object - :param List[_pytest.nodes.Item] items: list of item objects - """ - - -def pytest_collection_finish(session): - """ called after collection has been performed and modified. - - :param _pytest.main.Session session: the pytest session object - """ - - -@hookspec(firstresult=True) -def pytest_ignore_collect(path, config): - """ return True to prevent considering this path for collection. - 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 _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager + :param list[str] args: list of arguments passed on the command line + """ + + +def pytest_cmdline_preparse(config, args): + """(**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: pytest config object + :param list[str] args: list of arguments passed on the command line + """ + + +@hookspec(firstresult=True) +def pytest_cmdline_main(config): + """ 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. + + Stops at first non-None result, see :ref:`firstresult` + + :param _pytest.config.Config config: pytest config object + """ + + +def pytest_load_initial_conftests(early_config, parser, args): + """ implements 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. + + :param _pytest.config.Config early_config: pytest config object + :param list[str] args: list of arguments passed on the command line + :param _pytest.config.Parser parser: to add command line options + """ + + +# ------------------------------------------------------------------------- +# collection hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_collection(session): + """Perform the collection protocol for the given session. + + Stops at first non-None result, see :ref:`firstresult`. + + :param _pytest.main.Session session: the pytest session object + """ + + +def pytest_collection_modifyitems(session, config, items): + """ called after collection has been performed, may filter or re-order + the items in-place. + + :param _pytest.main.Session session: the pytest session object + :param _pytest.config.Config config: pytest config object + :param List[_pytest.nodes.Item] items: list of item objects + """ + + +def pytest_collection_finish(session): + """ called after collection has been performed and modified. + + :param _pytest.main.Session session: the pytest session object + """ + + +@hookspec(firstresult=True) +def pytest_ignore_collect(path, config): + """ return True to prevent considering this path for collection. + 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 path: a :py:class:`py.path.local` - the path to analyze - :param _pytest.config.Config config: pytest config object - """ - - -@hookspec(firstresult=True) -def pytest_collect_directory(path, parent): - """ called before traversing a directory for collection files. - - Stops at first non-None result, see :ref:`firstresult` - + :param _pytest.config.Config config: pytest config object + """ + + +@hookspec(firstresult=True) +def pytest_collect_directory(path, parent): + """ called before traversing a directory for collection files. + + Stops at first non-None result, see :ref:`firstresult` + :param path: a :py:class:`py.path.local` - the path to analyze - """ - - -def pytest_collect_file(path, parent): - """ return collection Node or None for the given path. Any new node - needs to have the specified ``parent`` as a parent. - + """ + + +def pytest_collect_file(path, parent): + """ return collection Node or None for the given path. Any new node + needs to have the specified ``parent`` as a parent. + :param path: a :py:class:`py.path.local` - the path to collect - """ - - -# logging hooks for collection - - -def pytest_collectstart(collector): - """ collector starts collecting. """ - - -def pytest_itemcollected(item): - """ we just collected a test item. """ - - -def pytest_collectreport(report): - """ collector finished collecting. """ - - -def pytest_deselected(items): + """ + + +# logging hooks for collection + + +def pytest_collectstart(collector): + """ collector starts collecting. """ + + +def pytest_itemcollected(item): + """ we just collected a test item. """ + + +def pytest_collectreport(report): + """ collector finished collecting. """ + + +def pytest_deselected(items): """ called for test items deselected, e.g. by keyword. """ - - -@hookspec(firstresult=True) -def pytest_make_collect_report(collector): - """ perform ``collector.collect()`` and return a CollectReport. - - Stops at first non-None result, see :ref:`firstresult` """ - - -# ------------------------------------------------------------------------- -# Python test function related hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) -def pytest_pycollect_makemodule(path, parent): - """ 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. - + + +@hookspec(firstresult=True) +def pytest_make_collect_report(collector): + """ perform ``collector.collect()`` and return a CollectReport. + + Stops at first non-None result, see :ref:`firstresult` """ + + +# ------------------------------------------------------------------------- +# Python test function related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_pycollect_makemodule(path, parent): + """ 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. + Stops at first non-None result, see :ref:`firstresult` - + :param path: a :py:class:`py.path.local` - the path of module to collect """ - - -@hookspec(firstresult=True) -def pytest_pycollect_makeitem(collector, name, obj): - """ return custom item/collector for a python object in a module, or None. - - Stops at first non-None result, see :ref:`firstresult` """ - - -@hookspec(firstresult=True) -def pytest_pyfunc_call(pyfuncitem): - """ call underlying test function. - - Stops at first non-None result, see :ref:`firstresult` """ - - -def pytest_generate_tests(metafunc): - """ generate (multiple) parametrized calls to a test function.""" - - -@hookspec(firstresult=True) -def pytest_make_parametrize_id(config, val, argname): - """Return a user-friendly string representation of the given ``val`` that will be used - by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. - The parameter name is available as ``argname``, if required. - - Stops at first non-None result, see :ref:`firstresult` - - :param _pytest.config.Config config: pytest config object - :param val: the parametrized value - :param str argname: the automatic parameter name produced by pytest - """ - - -# ------------------------------------------------------------------------- -# generic runtest related hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) -def pytest_runtestloop(session): - """ called for performing the main runtest loop - (after collection finished). - - Stops at first non-None result, see :ref:`firstresult` - - :param _pytest.main.Session session: the pytest session object - """ - - -def pytest_itemstart(item, node): - """(**Deprecated**) use pytest_runtest_logstart. """ - - -@hookspec(firstresult=True) -def pytest_runtest_protocol(item, nextitem): - """ implements the runtest_setup/call/teardown protocol for - the given test item, including capturing exceptions and calling - reporting hooks. - - :arg item: test item for which the runtest protocol is performed. - - :arg nextitem: the scheduled-to-be-next test item (or None if this - is the end my friend). This argument is passed on to - :py:func:`pytest_runtest_teardown`. - - :return boolean: True if no further hook implementations should be invoked. - - - Stops at first non-None result, see :ref:`firstresult` """ - - -def pytest_runtest_logstart(nodeid, location): - """ signal the start of running a single test item. - - This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and - :func:`pytest_runtest_teardown` hooks. - - :param str nodeid: full id of the item - :param location: a triple of ``(filename, linenum, testname)`` - """ - - -def pytest_runtest_logfinish(nodeid, location): - """ signal the complete finish of running a single test item. - - This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and - :func:`pytest_runtest_teardown` hooks. - - :param str nodeid: full id of the item - :param location: a triple of ``(filename, linenum, testname)`` - """ - - -def pytest_runtest_setup(item): - """ called before ``pytest_runtest_call(item)``. """ - - -def pytest_runtest_call(item): - """ called to execute the test ``item``. """ - - -def pytest_runtest_teardown(item, nextitem): - """ called after ``pytest_runtest_call``. - - :arg 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, i.e. calling just enough finalizers - so that nextitem only needs to call setup-functions. - """ - - -@hookspec(firstresult=True) -def pytest_runtest_makereport(item, call): - """ return a :py:class:`_pytest.runner.TestReport` object - for the given :py:class:`pytest.Item <_pytest.main.Item>` and - :py:class:`_pytest.runner.CallInfo`. - - Stops at first non-None result, see :ref:`firstresult` """ - - -def pytest_runtest_logreport(report): - """ process a test setup/call/teardown report relating to - the respective phase of executing a test. """ - - + + +@hookspec(firstresult=True) +def pytest_pycollect_makeitem(collector, name, obj): + """ return custom item/collector for a python object in a module, or None. + + Stops at first non-None result, see :ref:`firstresult` """ + + +@hookspec(firstresult=True) +def pytest_pyfunc_call(pyfuncitem): + """ call underlying test function. + + Stops at first non-None result, see :ref:`firstresult` """ + + +def pytest_generate_tests(metafunc): + """ generate (multiple) parametrized calls to a test function.""" + + +@hookspec(firstresult=True) +def pytest_make_parametrize_id(config, val, argname): + """Return a user-friendly string representation of the given ``val`` that will be used + by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``. + The parameter name is available as ``argname``, if required. + + Stops at first non-None result, see :ref:`firstresult` + + :param _pytest.config.Config config: pytest config object + :param val: the parametrized value + :param str argname: the automatic parameter name produced by pytest + """ + + +# ------------------------------------------------------------------------- +# generic runtest related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_runtestloop(session): + """ called for performing the main runtest loop + (after collection finished). + + Stops at first non-None result, see :ref:`firstresult` + + :param _pytest.main.Session session: the pytest session object + """ + + +def pytest_itemstart(item, node): + """(**Deprecated**) use pytest_runtest_logstart. """ + + +@hookspec(firstresult=True) +def pytest_runtest_protocol(item, nextitem): + """ implements the runtest_setup/call/teardown protocol for + the given test item, including capturing exceptions and calling + reporting hooks. + + :arg item: test item for which the runtest protocol is performed. + + :arg nextitem: the scheduled-to-be-next test item (or None if this + is the end my friend). This argument is passed on to + :py:func:`pytest_runtest_teardown`. + + :return boolean: True if no further hook implementations should be invoked. + + + Stops at first non-None result, see :ref:`firstresult` """ + + +def pytest_runtest_logstart(nodeid, location): + """ signal the start of running a single test item. + + This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and + :func:`pytest_runtest_teardown` hooks. + + :param str nodeid: full id of the item + :param location: a triple of ``(filename, linenum, testname)`` + """ + + +def pytest_runtest_logfinish(nodeid, location): + """ signal the complete finish of running a single test item. + + This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and + :func:`pytest_runtest_teardown` hooks. + + :param str nodeid: full id of the item + :param location: a triple of ``(filename, linenum, testname)`` + """ + + +def pytest_runtest_setup(item): + """ called before ``pytest_runtest_call(item)``. """ + + +def pytest_runtest_call(item): + """ called to execute the test ``item``. """ + + +def pytest_runtest_teardown(item, nextitem): + """ called after ``pytest_runtest_call``. + + :arg 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, i.e. calling just enough finalizers + so that nextitem only needs to call setup-functions. + """ + + +@hookspec(firstresult=True) +def pytest_runtest_makereport(item, call): + """ return a :py:class:`_pytest.runner.TestReport` object + for the given :py:class:`pytest.Item <_pytest.main.Item>` and + :py:class:`_pytest.runner.CallInfo`. + + Stops at first non-None result, see :ref:`firstresult` """ + + +def pytest_runtest_logreport(report): + """ process a test setup/call/teardown report relating to + the respective phase of executing a test. """ + + @hookspec(firstresult=True) def pytest_report_to_serializable(config, report): """ @@ -415,225 +415,225 @@ def pytest_report_from_serializable(config, data): """ -# ------------------------------------------------------------------------- -# Fixture related hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) -def pytest_fixture_setup(fixturedef, request): - """ performs fixture setup execution. - - :return: 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. - """ - - -def pytest_fixture_post_finalizer(fixturedef, request): - """ called after fixture teardown, but before the cache is cleared so - the fixture result cache ``fixturedef.cached_result`` can - still be accessed.""" - - -# ------------------------------------------------------------------------- -# test session related hooks -# ------------------------------------------------------------------------- - - -def pytest_sessionstart(session): - """ called after the ``Session`` object has been created and before performing collection - and entering the run test loop. - - :param _pytest.main.Session session: the pytest session object - """ - - -def pytest_sessionfinish(session, exitstatus): - """ called after whole test run finished, right before returning the exit status to the system. - - :param _pytest.main.Session session: the pytest session object - :param int exitstatus: the status which pytest will return to the system - """ - - -def pytest_unconfigure(config): - """ called before test process is exited. - - :param _pytest.config.Config config: pytest config object - """ - - -# ------------------------------------------------------------------------- -# hooks for customizing the assert methods -# ------------------------------------------------------------------------- - - -def pytest_assertrepr_compare(config, op, left, right): - """return explanation for comparisons in failing assert expressions. - - 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. - - :param _pytest.config.Config config: pytest config object - """ - - -# ------------------------------------------------------------------------- -# hooks for influencing reporting (invoked from _pytest_terminal) -# ------------------------------------------------------------------------- - - -def pytest_report_header(config, startdir): - """ return a string or list of strings to be displayed as header info for terminal reporting. - - :param _pytest.config.Config config: pytest config object - :param startdir: py.path object with the starting dir - - .. 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>`. - """ - - -def pytest_report_collectionfinish(config, startdir, items): - """ - .. versionadded:: 3.2 - - return a string or list of strings to be displayed after collection has finished successfully. - - This strings will be displayed after the standard "collected X items" message. - - :param _pytest.config.Config config: pytest config object - :param startdir: py.path object with the starting dir - :param items: list of pytest items that are going to be executed; this list should not be modified. - """ - - -@hookspec(firstresult=True) +# ------------------------------------------------------------------------- +# Fixture related hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_fixture_setup(fixturedef, request): + """ performs fixture setup execution. + + :return: 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. + """ + + +def pytest_fixture_post_finalizer(fixturedef, request): + """ called after fixture teardown, but before the cache is cleared so + the fixture result cache ``fixturedef.cached_result`` can + still be accessed.""" + + +# ------------------------------------------------------------------------- +# test session related hooks +# ------------------------------------------------------------------------- + + +def pytest_sessionstart(session): + """ called after the ``Session`` object has been created and before performing collection + and entering the run test loop. + + :param _pytest.main.Session session: the pytest session object + """ + + +def pytest_sessionfinish(session, exitstatus): + """ called after whole test run finished, right before returning the exit status to the system. + + :param _pytest.main.Session session: the pytest session object + :param int exitstatus: the status which pytest will return to the system + """ + + +def pytest_unconfigure(config): + """ called before test process is exited. + + :param _pytest.config.Config config: pytest config object + """ + + +# ------------------------------------------------------------------------- +# hooks for customizing the assert methods +# ------------------------------------------------------------------------- + + +def pytest_assertrepr_compare(config, op, left, right): + """return explanation for comparisons in failing assert expressions. + + 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. + + :param _pytest.config.Config config: pytest config object + """ + + +# ------------------------------------------------------------------------- +# hooks for influencing reporting (invoked from _pytest_terminal) +# ------------------------------------------------------------------------- + + +def pytest_report_header(config, startdir): + """ return a string or list of strings to be displayed as header info for terminal reporting. + + :param _pytest.config.Config config: pytest config object + :param startdir: py.path object with the starting dir + + .. 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>`. + """ + + +def pytest_report_collectionfinish(config, startdir, items): + """ + .. versionadded:: 3.2 + + return a string or list of strings to be displayed after collection has finished successfully. + + This strings will be displayed after the standard "collected X items" message. + + :param _pytest.config.Config config: pytest config object + :param startdir: py.path object with the starting dir + :param items: list of pytest items that are going to be executed; this list should not be modified. + """ + + +@hookspec(firstresult=True) def pytest_report_teststatus(report, config): - """ return result-category, shortletter and verbose word for reporting. - + """ return result-category, shortletter and verbose word for reporting. + :param _pytest.config.Config config: pytest config object - Stops at first non-None result, see :ref:`firstresult` """ - - + Stops at first non-None result, see :ref:`firstresult` """ + + def pytest_terminal_summary(terminalreporter, exitstatus, config): - """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 + """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: pytest config object - + .. versionadded:: 4.2 - The ``config`` parameter. - """ - - + The ``config`` parameter. + """ + + @hookspec(historic=True, warn_on_impl=PYTEST_LOGWARNING) -def pytest_logwarning(message, code, nodeid, fslocation): - """ - .. deprecated:: 3.8 - - This hook is will stop working in a future release. - - pytest no longer triggers this hook, but the - terminal writer still implements it to display warnings issued by - :meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be - an error in future releases. - - process a warning specified by a message, a code string, - a nodeid and fslocation (both of which may be None - if the warning is not tied to a particular node/location). - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - """ - - -@hookspec(historic=True) -def pytest_warning_captured(warning_message, when, item): - """ - Process a warning captured by the internal pytest warnings plugin. - - :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: - **DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None`` - in a future release. - - The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. - """ - - -# ------------------------------------------------------------------------- -# doctest hooks -# ------------------------------------------------------------------------- - - -@hookspec(firstresult=True) -def pytest_doctest_prepare_content(content): - """ return processed content for a given doctest - - Stops at first non-None result, see :ref:`firstresult` """ - - -# ------------------------------------------------------------------------- -# error handling and internal debugging hooks -# ------------------------------------------------------------------------- - - -def pytest_internalerror(excrepr, excinfo): - """ called for internal errors. """ - - -def pytest_keyboard_interrupt(excinfo): - """ called for keyboard interrupt. """ - - -def pytest_exception_interact(node, call, report): - """called when an exception was raised which can potentially be - interactively handled. - - This hook is only called if an exception was raised - that is not an internal exception like ``skip.Exception``. - """ - - -def pytest_enter_pdb(config, pdb): - """ called upon pdb.set_trace(), can be used by plugins to take special - action just before the python debugger enters in interactive mode. - - :param _pytest.config.Config config: pytest config object - :param pdb.Pdb pdb: Pdb instance - """ - - -def pytest_leave_pdb(config, pdb): - """ 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. - - :param _pytest.config.Config config: pytest config object - :param pdb.Pdb pdb: Pdb instance - """ +def pytest_logwarning(message, code, nodeid, fslocation): + """ + .. deprecated:: 3.8 + + This hook is will stop working in a future release. + + pytest no longer triggers this hook, but the + terminal writer still implements it to display warnings issued by + :meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be + an error in future releases. + + process a warning specified by a message, a code string, + a nodeid and fslocation (both of which may be None + if the warning is not tied to a particular node/location). + + .. note:: + This hook is incompatible with ``hookwrapper=True``. + """ + + +@hookspec(historic=True) +def pytest_warning_captured(warning_message, when, item): + """ + Process a warning captured by the internal pytest warnings plugin. + + :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: + **DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None`` + in a future release. + + The item being executed if ``when`` is ``"runtest"``, otherwise ``None``. + """ + + +# ------------------------------------------------------------------------- +# doctest hooks +# ------------------------------------------------------------------------- + + +@hookspec(firstresult=True) +def pytest_doctest_prepare_content(content): + """ return processed content for a given doctest + + Stops at first non-None result, see :ref:`firstresult` """ + + +# ------------------------------------------------------------------------- +# error handling and internal debugging hooks +# ------------------------------------------------------------------------- + + +def pytest_internalerror(excrepr, excinfo): + """ called for internal errors. """ + + +def pytest_keyboard_interrupt(excinfo): + """ called for keyboard interrupt. """ + + +def pytest_exception_interact(node, call, report): + """called when an exception was raised which can potentially be + interactively handled. + + This hook is only called if an exception was raised + that is not an internal exception like ``skip.Exception``. + """ + + +def pytest_enter_pdb(config, pdb): + """ called upon pdb.set_trace(), can be used by plugins to take special + action just before the python debugger enters in interactive mode. + + :param _pytest.config.Config config: pytest config object + :param pdb.Pdb pdb: Pdb instance + """ + + +def pytest_leave_pdb(config, pdb): + """ 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. + + :param _pytest.config.Config config: pytest config object + :param pdb.Pdb pdb: Pdb instance + """ diff --git a/contrib/python/pytest/py2/_pytest/junitxml.py b/contrib/python/pytest/py2/_pytest/junitxml.py index 8ae968f07b..853dcb7744 100644 --- a/contrib/python/pytest/py2/_pytest/junitxml.py +++ b/contrib/python/pytest/py2/_pytest/junitxml.py @@ -1,74 +1,74 @@ # -*- coding: utf-8 -*- -""" - report test results in JUnit-XML format, - for use with Jenkins and build integration servers. - - -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 -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import functools -import os +""" + report test results in JUnit-XML format, + for use with Jenkins and build integration servers. + + +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 +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import functools +import os import platform -import re -import sys -import time +import re +import sys +import time from datetime import datetime - -import py + +import py import six - -import pytest -from _pytest import nodes -from _pytest.config import filename_arg - -# Python 2.X and 3.X compatibility -if sys.version_info[0] < 3: - from codecs import open - - -class Junit(py.xml.Namespace): - pass - - -# We need to get the subset of the invalid unicode ranges according to -# XML 1.0 which are valid in this python build. Hence we calculate -# this dynamically instead of hardcoding it. The spec range of valid -# chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] -# | [#x10000-#x10FFFF] -_legal_chars = (0x09, 0x0A, 0x0D) -_legal_ranges = ((0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF)) -_legal_xml_re = [ + +import pytest +from _pytest import nodes +from _pytest.config import filename_arg + +# Python 2.X and 3.X compatibility +if sys.version_info[0] < 3: + from codecs import open + + +class Junit(py.xml.Namespace): + pass + + +# We need to get the subset of the invalid unicode ranges according to +# XML 1.0 which are valid in this python build. Hence we calculate +# this dynamically instead of hardcoding it. The spec range of valid +# chars is: Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] +# | [#x10000-#x10FFFF] +_legal_chars = (0x09, 0x0A, 0x0D) +_legal_ranges = ((0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF)) +_legal_xml_re = [ u"%s-%s" % (six.unichr(low), six.unichr(high)) - for (low, high) in _legal_ranges - if low < sys.maxunicode -] + for (low, high) in _legal_ranges + if low < sys.maxunicode +] _legal_xml_re = [six.unichr(x) for x in _legal_chars] + _legal_xml_re illegal_xml_re = re.compile(u"[^%s]" % u"".join(_legal_xml_re)) -del _legal_chars -del _legal_ranges -del _legal_xml_re - -_py_ext_re = re.compile(r"\.py$") - - -def bin_xml_escape(arg): - def repl(matchobj): - i = ord(matchobj.group()) - if i <= 0xFF: +del _legal_chars +del _legal_ranges +del _legal_xml_re + +_py_ext_re = re.compile(r"\.py$") + + +def bin_xml_escape(arg): + def repl(matchobj): + i = ord(matchobj.group()) + if i <= 0xFF: return u"#x%02X" % i - else: + else: return u"#x%04X" % i - - return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) - - + + return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) + + def merge_family(left, right): result = {} for kl, vl in left.items(): @@ -91,59 +91,59 @@ merge_family(families["xunit1"], families["_base_legacy"]) families["xunit2"] = families["_base"] -class _NodeReporter(object): - def __init__(self, nodeid, xml): - self.id = nodeid - self.xml = xml - self.add_stats = self.xml.add_stats +class _NodeReporter(object): + def __init__(self, nodeid, xml): + self.id = nodeid + self.xml = xml + self.add_stats = self.xml.add_stats self.family = self.xml.family - self.duration = 0 - self.properties = [] - self.nodes = [] - self.testcase = None - self.attrs = {} - - def append(self, node): - self.xml.add_stats(type(node).__name__) - self.nodes.append(node) - - def add_property(self, name, value): - self.properties.append((str(name), bin_xml_escape(value))) - - def add_attribute(self, name, value): - self.attrs[str(name)] = bin_xml_escape(value) - - def make_properties_node(self): - """Return a Junit node containing custom properties, if any. - """ - if self.properties: - return Junit.properties( - [ - Junit.property(name=name, value=value) - for name, value in self.properties - ] - ) - return "" - - def record_testreport(self, testreport): - assert not self.testcase - names = mangle_test_address(testreport.nodeid) - existing_attrs = self.attrs - classnames = names[:-1] - if self.xml.prefix: - classnames.insert(0, self.xml.prefix) - attrs = { - "classname": ".".join(classnames), - "name": bin_xml_escape(names[-1]), - "file": testreport.location[0], - } - if testreport.location[1] is not None: - attrs["line"] = testreport.location[1] - if hasattr(testreport, "url"): - attrs["url"] = testreport.url - self.attrs = attrs - self.attrs.update(existing_attrs) # restore any user-defined attributes - + self.duration = 0 + self.properties = [] + self.nodes = [] + self.testcase = None + self.attrs = {} + + def append(self, node): + self.xml.add_stats(type(node).__name__) + self.nodes.append(node) + + def add_property(self, name, value): + self.properties.append((str(name), bin_xml_escape(value))) + + def add_attribute(self, name, value): + self.attrs[str(name)] = bin_xml_escape(value) + + def make_properties_node(self): + """Return a Junit node containing custom properties, if any. + """ + if self.properties: + return Junit.properties( + [ + Junit.property(name=name, value=value) + for name, value in self.properties + ] + ) + return "" + + def record_testreport(self, testreport): + assert not self.testcase + names = mangle_test_address(testreport.nodeid) + existing_attrs = self.attrs + classnames = names[:-1] + if self.xml.prefix: + classnames.insert(0, self.xml.prefix) + attrs = { + "classname": ".".join(classnames), + "name": bin_xml_escape(names[-1]), + "file": testreport.location[0], + } + if testreport.location[1] is not None: + attrs["line"] = testreport.location[1] + 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 @@ -156,108 +156,108 @@ class _NodeReporter(object): temp_attrs[key] = self.attrs[key] self.attrs = temp_attrs - def to_xml(self): + def to_xml(self): testcase = Junit.testcase(time="%.3f" % self.duration, **self.attrs) - testcase.append(self.make_properties_node()) - for node in self.nodes: - testcase.append(node) - return testcase - - def _add_simple(self, kind, message, data=None): - data = bin_xml_escape(data) - node = kind(data, message=message) - self.append(node) - - def write_captured_output(self, report): + testcase.append(self.make_properties_node()) + for node in self.nodes: + testcase.append(node) + return testcase + + def _add_simple(self, kind, message, data=None): + data = bin_xml_escape(data) + node = kind(data, message=message) + self.append(node) + + def write_captured_output(self, report): if not self.xml.log_passing_tests and report.passed: return - content_out = report.capstdout - content_log = report.caplog - content_err = report.capstderr - - if content_log or content_out: - if content_log and self.xml.logging == "system-out": - if content_out: - # syncing stdout and the log-output is not done yet. It's - # probably not worth the effort. Therefore, first the captured - # stdout is shown and then the captured logs. - content = "\n".join( - [ - " Captured Stdout ".center(80, "-"), - content_out, - "", - " Captured Log ".center(80, "-"), - content_log, - ] - ) - else: - content = content_log - else: - content = content_out - - if content: - tag = getattr(Junit, "system-out") - self.append(tag(bin_xml_escape(content))) - - if content_log or content_err: - if content_log and self.xml.logging == "system-err": - if content_err: - content = "\n".join( - [ - " Captured Stderr ".center(80, "-"), - content_err, - "", - " Captured Log ".center(80, "-"), - content_log, - ] - ) - else: - content = content_log - else: - content = content_err - - if content: - tag = getattr(Junit, "system-err") - self.append(tag(bin_xml_escape(content))) - - def append_pass(self, report): - self.add_stats("passed") - - def append_failure(self, report): - # msg = str(report.longrepr.reprtraceback.extraline) - if hasattr(report, "wasxfail"): - self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly") - else: - if hasattr(report.longrepr, "reprcrash"): - message = report.longrepr.reprcrash.message + content_out = report.capstdout + content_log = report.caplog + content_err = report.capstderr + + if content_log or content_out: + if content_log and self.xml.logging == "system-out": + if content_out: + # syncing stdout and the log-output is not done yet. It's + # probably not worth the effort. Therefore, first the captured + # stdout is shown and then the captured logs. + content = "\n".join( + [ + " Captured Stdout ".center(80, "-"), + content_out, + "", + " Captured Log ".center(80, "-"), + content_log, + ] + ) + else: + content = content_log + else: + content = content_out + + if content: + tag = getattr(Junit, "system-out") + self.append(tag(bin_xml_escape(content))) + + if content_log or content_err: + if content_log and self.xml.logging == "system-err": + if content_err: + content = "\n".join( + [ + " Captured Stderr ".center(80, "-"), + content_err, + "", + " Captured Log ".center(80, "-"), + content_log, + ] + ) + else: + content = content_log + else: + content = content_err + + if content: + tag = getattr(Junit, "system-err") + self.append(tag(bin_xml_escape(content))) + + def append_pass(self, report): + self.add_stats("passed") + + def append_failure(self, report): + # msg = str(report.longrepr.reprtraceback.extraline) + if hasattr(report, "wasxfail"): + self._add_simple(Junit.skipped, "xfail-marked test passes unexpectedly") + else: + if hasattr(report.longrepr, "reprcrash"): + message = report.longrepr.reprcrash.message elif isinstance(report.longrepr, six.string_types): - message = report.longrepr - else: - message = str(report.longrepr) - message = bin_xml_escape(message) - fail = Junit.failure(message=message) - fail.append(bin_xml_escape(report.longrepr)) - self.append(fail) - - def append_collect_error(self, report): - # msg = str(report.longrepr.reprtraceback.extraline) - self.append( - Junit.error(bin_xml_escape(report.longrepr), message="collection failure") - ) - - def append_collect_skipped(self, report): - self._add_simple(Junit.skipped, "collection skipped", report.longrepr) - - def append_error(self, report): + message = report.longrepr + else: + message = str(report.longrepr) + message = bin_xml_escape(message) + fail = Junit.failure(message=message) + fail.append(bin_xml_escape(report.longrepr)) + self.append(fail) + + def append_collect_error(self, report): + # msg = str(report.longrepr.reprtraceback.extraline) + self.append( + Junit.error(bin_xml_escape(report.longrepr), message="collection failure") + ) + + def append_collect_skipped(self, report): + self._add_simple(Junit.skipped, "collection skipped", report.longrepr) + + def append_error(self, report): if report.when == "teardown": - msg = "test teardown failure" - else: - msg = "test setup failure" - self._add_simple(Junit.error, msg, report.longrepr) - - def append_skipped(self, report): - if hasattr(report, "wasxfail"): + msg = "test teardown failure" + else: + msg = "test setup failure" + self._add_simple(Junit.error, msg, report.longrepr) + + def append_skipped(self, report): + if hasattr(report, "wasxfail"): xfailreason = report.wasxfail if xfailreason.startswith("reason: "): xfailreason = xfailreason[8:] @@ -266,27 +266,27 @@ class _NodeReporter(object): "", type="pytest.xfail", message=bin_xml_escape(xfailreason) ) ) - else: - filename, lineno, skipreason = report.longrepr - if skipreason.startswith("Skipped: "): - skipreason = skipreason[9:] - details = "%s:%s: %s" % (filename, lineno, skipreason) - - self.append( - Junit.skipped( - bin_xml_escape(details), - type="pytest.skip", - message=bin_xml_escape(skipreason), - ) - ) - self.write_captured_output(report) - - def finalize(self): - data = self.to_xml().unicode(indent=0) - self.__dict__.clear() - self.to_xml = lambda: py.xml.raw(data) - - + else: + filename, lineno, skipreason = report.longrepr + if skipreason.startswith("Skipped: "): + skipreason = skipreason[9:] + details = "%s:%s: %s" % (filename, lineno, skipreason) + + self.append( + Junit.skipped( + bin_xml_escape(details), + type="pytest.skip", + message=bin_xml_escape(skipreason), + ) + ) + self.write_captured_output(report) + + def finalize(self): + data = self.to_xml().unicode(indent=0) + self.__dict__.clear() + self.to_xml = lambda: py.xml.raw(data) + + def _warn_incompatibility_with_xunit2(request, fixture_name): """Emits a PytestWarning about the given fixture being incompatible with newer xunit revisions""" from _pytest.warning_types import PytestWarning @@ -302,35 +302,35 @@ def _warn_incompatibility_with_xunit2(request, fixture_name): ) -@pytest.fixture -def record_property(request): - """Add an extra properties the calling test. - 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)``, with value being automatically - xml-encoded. - - Example:: - - def test_function(record_property): - record_property("example_key", 1) - """ +@pytest.fixture +def record_property(request): + """Add an extra properties the calling test. + 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)``, with value being automatically + xml-encoded. + + Example:: + + def test_function(record_property): + record_property("example_key", 1) + """ _warn_incompatibility_with_xunit2(request, "record_property") - - def append_property(name, value): - request.node.user_properties.append((name, value)) - - return append_property - - -@pytest.fixture -def record_xml_attribute(request): - """Add extra xml attributes to the tag for the calling test. - The fixture is callable with ``(name, value)``, with value being - automatically xml-encoded - """ + + def append_property(name, value): + request.node.user_properties.append((name, value)) + + return append_property + + +@pytest.fixture +def record_xml_attribute(request): + """Add extra xml attributes to the tag for the calling test. + The fixture is callable with ``(name, value)``, with value being + automatically xml-encoded + """ from _pytest.warning_types import PytestExperimentalApiWarning - + request.node.warn( PytestExperimentalApiWarning("record_xml_attribute is an experimental feature") ) @@ -343,14 +343,14 @@ def record_xml_attribute(request): attr_func = add_attr_noop - xml = getattr(request.config, "_xml", None) - if xml is not None: - node_reporter = xml.node_reporter(request.node.nodeid) + xml = getattr(request.config, "_xml", None) + 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, v): """Used by record_testsuite_property to check that the given parameter name is of the proper type""" @@ -358,7 +358,7 @@ def _check_record_param_type(param, v): if not isinstance(v, six.string_types): msg = "{param} parameter needs to be a string, but {g} given" raise TypeError(msg.format(param=param, g=type(v).__name__)) - + @pytest.fixture(scope="session") def record_testsuite_property(request): @@ -390,35 +390,35 @@ def record_testsuite_property(request): return record_func -def pytest_addoption(parser): - 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|system-out|system-err", - default="no", - ) # choices=['no', 'stdout', 'stderr']) +def pytest_addoption(parser): + 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|system-out|system-err", + default="no", + ) # choices=['no', 'stdout', 'stderr']) parser.addini( "junit_log_passing_tests", "Capture log information for passing tests to JUnit report: ", @@ -435,47 +435,47 @@ def pytest_addoption(parser): "Emit XML for schema: one of legacy|xunit1|xunit2", default="xunit1", ) - - -def pytest_configure(config): - xmlpath = config.option.xmlpath - # prevent opening xmllog on slave nodes (xdist) - if xmlpath and not hasattr(config, "slaveinput"): - config._xml = LogXML( - xmlpath, - config.option.junitprefix, - config.getini("junit_suite_name"), - config.getini("junit_logging"), + + +def pytest_configure(config): + xmlpath = config.option.xmlpath + # prevent opening xmllog on slave nodes (xdist) + if xmlpath and not hasattr(config, "slaveinput"): + config._xml = LogXML( + xmlpath, + config.option.junitprefix, + config.getini("junit_suite_name"), + config.getini("junit_logging"), config.getini("junit_duration_report"), config.getini("junit_family"), config.getini("junit_log_passing_tests"), - ) - config.pluginmanager.register(config._xml) - - -def pytest_unconfigure(config): - xml = getattr(config, "_xml", None) - if xml: - del config._xml - config.pluginmanager.unregister(xml) - - -def mangle_test_address(address): - 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] = _py_ext_re.sub("", names[0]) - # put any params back - names[-1] += possible_open_bracket + params - return names - - -class LogXML(object): + ) + config.pluginmanager.register(config._xml) + + +def pytest_unconfigure(config): + xml = getattr(config, "_xml", None) + if xml: + del config._xml + config.pluginmanager.unregister(xml) + + +def mangle_test_address(address): + 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] = _py_ext_re.sub("", names[0]) + # put any params back + names[-1] += possible_open_bracket + params + return names + + +class LogXML(object): def __init__( self, logfile, @@ -486,191 +486,191 @@ class LogXML(object): family="xunit1", log_passing_tests=True, ): - 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 - self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) - self.node_reporters = {} # nodeid -> _NodeReporter - self.node_reporters_ordered = [] - self.global_properties = [] - - # List of reports that failed on call but teardown is pending. - self.open_reports = [] - self.cnt_double_fail_tests = 0 - + self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) + self.node_reporters = {} # nodeid -> _NodeReporter + self.node_reporters_ordered = [] + self.global_properties = [] + + # List of reports that failed on call but teardown is pending. + self.open_reports = [] + self.cnt_double_fail_tests = 0 + # Replaces convenience family with real family if self.family == "legacy": self.family = "xunit1" - def finalize(self, report): - nodeid = getattr(report, "nodeid", report) - # local hack to handle xdist report order - slavenode = getattr(report, "node", None) - reporter = self.node_reporters.pop((nodeid, slavenode)) - if reporter is not None: - reporter.finalize() - - def node_reporter(self, report): - nodeid = getattr(report, "nodeid", report) - # local hack to handle xdist report order - slavenode = getattr(report, "node", None) - - key = nodeid, slavenode - - if key in self.node_reporters: - # TODO: breasks 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 - - def add_stats(self, key): - if key in self.stats: - self.stats[key] += 1 - - def _opentestcase(self, report): - reporter = self.node_reporter(report) - reporter.record_testreport(report) - return reporter - - def pytest_runtest_logreport(self, report): - """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 - - 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": - # 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 - # 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) + def finalize(self, report): + nodeid = getattr(report, "nodeid", report) + # local hack to handle xdist report order + slavenode = getattr(report, "node", None) + reporter = self.node_reporters.pop((nodeid, slavenode)) + if reporter is not None: + reporter.finalize() + + def node_reporter(self, report): + nodeid = getattr(report, "nodeid", report) + # local hack to handle xdist report order + slavenode = getattr(report, "node", None) + + key = nodeid, slavenode + + if key in self.node_reporters: + # TODO: breasks 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 + + def add_stats(self, key): + if key in self.stats: + self.stats[key] += 1 + + def _opentestcase(self, report): + reporter = self.node_reporter(report) + reporter.record_testreport(report) + return reporter + + def pytest_runtest_logreport(self, report): + """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 + + 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": + # 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 + # 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) 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: - reporter.add_property(propname, 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) - - def update_testcase_duration(self, report): - """accumulates total duration for nodeid from given report and updates - the Junit.testcase with the new total if already created. - """ + 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, 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) + + def update_testcase_duration(self, report): + """accumulates total duration for nodeid from given report and updates + 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): - 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): - reporter = self.node_reporter("internal") - reporter.attrs.update(classname="pytest", name="internal") - reporter._add_simple(Junit.error, "internal error", excrepr) - - def pytest_sessionstart(self): - self.suite_start_time = time.time() - - def pytest_sessionfinish(self): - 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 = time.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"?>') - + + def pytest_collectreport(self, 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): + reporter = self.node_reporter("internal") + reporter.attrs.update(classname="pytest", name="internal") + reporter._add_simple(Junit.error, "internal error", excrepr) + + def pytest_sessionstart(self): + self.suite_start_time = time.time() + + def pytest_sessionfinish(self): + 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 = time.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_node = Junit.testsuite( self._get_global_properties_node(), [x.to_xml() for x in self.node_reporters_ordered], @@ -682,26 +682,26 @@ class LogXML(object): time="%.3f" % suite_time_delta, timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), hostname=platform.node(), - ) + ) logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) - logfile.close() - - def pytest_terminal_summary(self, terminalreporter): - terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) - - def add_global_property(self, name, value): + logfile.close() + + def pytest_terminal_summary(self, terminalreporter): + terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) + + def add_global_property(self, name, value): __tracebackhide__ = True _check_record_param_type("name", name) self.global_properties.append((name, bin_xml_escape(value))) - - def _get_global_properties_node(self): - """Return a Junit node containing custom properties, if any. - """ - if self.global_properties: - return Junit.properties( - [ - Junit.property(name=name, value=value) - for name, value in self.global_properties - ] - ) - return "" + + def _get_global_properties_node(self): + """Return a Junit node containing custom properties, if any. + """ + if self.global_properties: + return Junit.properties( + [ + Junit.property(name=name, value=value) + for name, value in self.global_properties + ] + ) + return "" diff --git a/contrib/python/pytest/py2/_pytest/logging.py b/contrib/python/pytest/py2/_pytest/logging.py index 3e57bc42eb..2400737ee4 100644 --- a/contrib/python/pytest/py2/_pytest/logging.py +++ b/contrib/python/pytest/py2/_pytest/logging.py @@ -1,82 +1,82 @@ # -*- coding: utf-8 -*- -""" Access and control log capturing. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import logging -import re -from contextlib import contextmanager - -import py -import six - -import pytest -from _pytest.compat import dummy_context_manager -from _pytest.config import create_terminal_writer +""" Access and control log capturing. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import logging +import re +from contextlib import contextmanager + +import py +import six + +import pytest +from _pytest.compat import dummy_context_manager +from _pytest.config import create_terminal_writer from _pytest.pathlib import Path - + 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") - - + + def _remove_ansi_escape_sequences(text): return _ANSI_ESCAPE_SEQ.sub("", text) -class ColoredLevelFormatter(logging.Formatter): - """ - Colorize the %(levelname)..s part of the log format passed to __init__. - """ - - LOGLEVEL_COLOROPTS = { - 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, *args, **kwargs): - super(ColoredLevelFormatter, self).__init__(*args, **kwargs) - if six.PY2: - self._original_fmt = self._fmt - else: - self._original_fmt = self._style._fmt - self._level_to_fmt_mapping = {} - - 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): - fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt) - if six.PY2: - self._fmt = fmt - else: - self._style._fmt = fmt - return super(ColoredLevelFormatter, self).format(record) - - +class ColoredLevelFormatter(logging.Formatter): + """ + Colorize the %(levelname)..s part of the log format passed to __init__. + """ + + LOGLEVEL_COLOROPTS = { + 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, *args, **kwargs): + super(ColoredLevelFormatter, self).__init__(*args, **kwargs) + if six.PY2: + self._original_fmt = self._fmt + else: + self._original_fmt = self._style._fmt + self._level_to_fmt_mapping = {} + + 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): + fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt) + if six.PY2: + self._fmt = fmt + else: + self._style._fmt = fmt + return super(ColoredLevelFormatter, self).format(record) + + if not six.PY2: # Formatter classes don't support format styles in PY2 @@ -107,330 +107,330 @@ if not six.PY2: return self._fmt % record.__dict__ -def get_option_ini(config, *names): - 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): - """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( - "--no-print-logs", - dest="log_print", - action="store_const", - const=False, - default=True, - type="bool", - help="disable printing caught logs on failed tests.", - ) - add_option_ini( - "--log-level", - dest="log_level", - default=None, - help="logging level 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.", - ) - - -@contextmanager -def catching_logs(handler, formatter=None, level=None): - """Context manager that prepares the whole logging machinery properly.""" - root_logger = logging.getLogger() - - if formatter is not None: - handler.setFormatter(formatter) - if level is not None: - handler.setLevel(level) - - # Adding the same handler twice would confuse logging system. - # Just don't do that. - add_new_handler = handler not in root_logger.handlers - - if add_new_handler: - root_logger.addHandler(handler) - if level is not None: - orig_level = root_logger.level - root_logger.setLevel(min(orig_level, level)) - try: - yield handler - finally: - if level is not None: - root_logger.setLevel(orig_level) - if add_new_handler: - root_logger.removeHandler(handler) - - -class LogCaptureHandler(logging.StreamHandler): - """A logging handler that stores log records and the log text.""" - - def __init__(self): - """Creates a new log handler.""" - logging.StreamHandler.__init__(self, py.io.TextIO()) - self.records = [] - - def emit(self, record): - """Keep the log records in a list in addition to the log text.""" - self.records.append(record) - logging.StreamHandler.emit(self, record) - - def reset(self): - self.records = [] - self.stream = py.io.TextIO() - - -class LogCaptureFixture(object): - """Provides access and control of log capturing.""" - - def __init__(self, item): - """Creates a new funcarg.""" - self._item = item - # dict of log name -> log level +def get_option_ini(config, *names): + 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): + """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( + "--no-print-logs", + dest="log_print", + action="store_const", + const=False, + default=True, + type="bool", + help="disable printing caught logs on failed tests.", + ) + add_option_ini( + "--log-level", + dest="log_level", + default=None, + help="logging level 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.", + ) + + +@contextmanager +def catching_logs(handler, formatter=None, level=None): + """Context manager that prepares the whole logging machinery properly.""" + root_logger = logging.getLogger() + + if formatter is not None: + handler.setFormatter(formatter) + if level is not None: + handler.setLevel(level) + + # Adding the same handler twice would confuse logging system. + # Just don't do that. + add_new_handler = handler not in root_logger.handlers + + if add_new_handler: + root_logger.addHandler(handler) + if level is not None: + orig_level = root_logger.level + root_logger.setLevel(min(orig_level, level)) + try: + yield handler + finally: + if level is not None: + root_logger.setLevel(orig_level) + if add_new_handler: + root_logger.removeHandler(handler) + + +class LogCaptureHandler(logging.StreamHandler): + """A logging handler that stores log records and the log text.""" + + def __init__(self): + """Creates a new log handler.""" + logging.StreamHandler.__init__(self, py.io.TextIO()) + self.records = [] + + def emit(self, record): + """Keep the log records in a list in addition to the log text.""" + self.records.append(record) + logging.StreamHandler.emit(self, record) + + def reset(self): + self.records = [] + self.stream = py.io.TextIO() + + +class LogCaptureFixture(object): + """Provides access and control of log capturing.""" + + def __init__(self, item): + """Creates a new funcarg.""" + self._item = item + # dict of log name -> log level self._initial_log_levels = {} # Dict[str, int] - - def _finalize(self): - """Finalizes the fixture. - - This restores the log levels changed by :meth:`set_level`. - """ - # restore log levels - for logger_name, level in self._initial_log_levels.items(): - logger = logging.getLogger(logger_name) - logger.setLevel(level) - - @property - def handler(self): - """ - :rtype: LogCaptureHandler - """ - return self._item.catch_log_handler - - def get_records(self, when): - """ - 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". - - :rtype: List[logging.LogRecord] - :return: the list of captured records at the given stage - - .. versionadded:: 3.4 - """ - handler = self._item.catch_log_handlers.get(when) - if handler: - return handler.records - else: - return [] - - @property - def text(self): + + def _finalize(self): + """Finalizes the fixture. + + This restores the log levels changed by :meth:`set_level`. + """ + # restore log levels + for logger_name, level in self._initial_log_levels.items(): + logger = logging.getLogger(logger_name) + logger.setLevel(level) + + @property + def handler(self): + """ + :rtype: LogCaptureHandler + """ + return self._item.catch_log_handler + + def get_records(self, when): + """ + 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". + + :rtype: List[logging.LogRecord] + :return: the list of captured records at the given stage + + .. versionadded:: 3.4 + """ + handler = self._item.catch_log_handlers.get(when) + if handler: + return handler.records + else: + return [] + + @property + def text(self): """Returns the formatted log text.""" return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) - - @property - def records(self): - """Returns the list of log records.""" - return self.handler.records - - @property - def record_tuples(self): - """Returns 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 - def messages(self): - """Returns 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] - - def clear(self): - """Reset the list of log records and the captured log text.""" - self.handler.reset() - - def set_level(self, level, logger=None): - """Sets the level for capturing of logs. The level will be restored to its previous value at the end of - the test. - - :param int level: the logger to level. - :param str logger: the logger to update the level. If not given, the root logger level is updated. - - .. versionchanged:: 3.4 - The levels of the loggers changed by this function will be restored to their initial values at the - end of the test. - """ - logger_name = logger - logger = logging.getLogger(logger_name) - # save the original log-level to restore it during teardown - self._initial_log_levels.setdefault(logger_name, logger.level) - logger.setLevel(level) - - @contextmanager - def at_level(self, level, logger=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 logger to level. - :param str logger: the logger to update the level. If not given, the root logger level is updated. - """ - logger = logging.getLogger(logger) - orig_level = logger.level - logger.setLevel(level) - try: - yield - finally: - logger.setLevel(orig_level) - - -@pytest.fixture -def caplog(request): - """Access and control log capturing. - - Captured logs are available through the following properties/methods:: - - * 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) - yield result - result._finalize() - - -def get_actual_log_level(config, *setting_names): - """Return the actual logging level.""" - - 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 - - if isinstance(log_level, six.string_types): - log_level = log_level.upper() - try: - return int(getattr(logging, log_level, log_level)) - except ValueError: - # Python logging does not recognise this as a logging level - raise pytest.UsageError( - "'{}' is not recognized as a logging level name for " - "'{}'. Please consider passing the " - "logging level num instead.".format(log_level, setting_name) - ) - - + + @property + def records(self): + """Returns the list of log records.""" + return self.handler.records + + @property + def record_tuples(self): + """Returns 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 + def messages(self): + """Returns 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] + + def clear(self): + """Reset the list of log records and the captured log text.""" + self.handler.reset() + + def set_level(self, level, logger=None): + """Sets the level for capturing of logs. The level will be restored to its previous value at the end of + the test. + + :param int level: the logger to level. + :param str logger: the logger to update the level. If not given, the root logger level is updated. + + .. versionchanged:: 3.4 + The levels of the loggers changed by this function will be restored to their initial values at the + end of the test. + """ + logger_name = logger + logger = logging.getLogger(logger_name) + # save the original log-level to restore it during teardown + self._initial_log_levels.setdefault(logger_name, logger.level) + logger.setLevel(level) + + @contextmanager + def at_level(self, level, logger=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 logger to level. + :param str logger: the logger to update the level. If not given, the root logger level is updated. + """ + logger = logging.getLogger(logger) + orig_level = logger.level + logger.setLevel(level) + try: + yield + finally: + logger.setLevel(orig_level) + + +@pytest.fixture +def caplog(request): + """Access and control log capturing. + + Captured logs are available through the following properties/methods:: + + * 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) + yield result + result._finalize() + + +def get_actual_log_level(config, *setting_names): + """Return the actual logging level.""" + + 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 + + if isinstance(log_level, six.string_types): + log_level = log_level.upper() + try: + return int(getattr(logging, log_level, log_level)) + except ValueError: + # Python logging does not recognise this as a logging level + raise pytest.UsageError( + "'{}' is not recognized as a logging level name for " + "'{}'. Please consider passing the " + "logging level num instead.".format(log_level, setting_name) + ) + + # run after terminalreporter/capturemanager are configured @pytest.hookimpl(trylast=True) -def pytest_configure(config): - config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") - - -class LoggingPlugin(object): - """Attaches to the logging module and captures log messages for each test. - """ - - def __init__(self, config): - """Creates 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 - - self.print_logs = get_option_ini(config, "log_print") +def pytest_configure(config): + config.pluginmanager.register(LoggingPlugin(config), "logging-plugin") + + +class LoggingPlugin(object): + """Attaches to the logging module and captures log messages for each test. + """ + + def __init__(self, config): + """Creates 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 + + self.print_logs = get_option_ini(config, "log_print") self.formatter = self._create_formatter( - get_option_ini(config, "log_format"), - get_option_ini(config, "log_date_format"), - ) - self.log_level = get_actual_log_level(config, "log_level") - + get_option_ini(config, "log_format"), + get_option_ini(config, "log_date_format"), + ) + self.log_level = get_actual_log_level(config, "log_level") + self.log_file_level = get_actual_log_level(config, "log_file_level") self.log_file_format = get_option_ini(config, "log_file_format", "log_format") self.log_file_date_format = get_option_ini( @@ -440,17 +440,17 @@ class LoggingPlugin(object): self.log_file_format, datefmt=self.log_file_date_format ) - log_file = get_option_ini(config, "log_file") - if log_file: - self.log_file_handler = logging.FileHandler( - log_file, mode="w", encoding="UTF-8" - ) + log_file = get_option_ini(config, "log_file") + if log_file: + self.log_file_handler = logging.FileHandler( + log_file, mode="w", encoding="UTF-8" + ) self.log_file_handler.setFormatter(self.log_file_formatter) - else: - self.log_file_handler = None - - self.log_cli_handler = None - + else: + self.log_file_handler = None + + self.log_cli_handler = None + self.live_logs_context = lambda: dummy_context_manager() # Note that the lambda for the live_logs_context is needed because # live_logs_context can otherwise not be entered multiple times due @@ -518,28 +518,28 @@ class LoggingPlugin(object): ) self.log_file_handler.setFormatter(self.log_file_formatter) - def _log_cli_enabled(self): - """Return True if log_cli should be considered enabled, either explicitly - or because --log-cli-level was given in the command-line. - """ - return self._config.getoption( - "--log-cli-level" - ) is not None or self._config.getini("log_cli") - - @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_collection(self): - with self.live_logs_context(): - if self.log_cli_handler: - self.log_cli_handler.set_when("collection") - - if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - else: - yield - - @contextmanager - def _runtest_for(self, item, when): + def _log_cli_enabled(self): + """Return True if log_cli should be considered enabled, either explicitly + or because --log-cli-level was given in the command-line. + """ + return self._config.getoption( + "--log-cli-level" + ) is not None or self._config.getini("log_cli") + + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_collection(self): + with self.live_logs_context(): + if self.log_cli_handler: + self.log_cli_handler.set_when("collection") + + if self.log_file_handler is not None: + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield + else: + yield + + @contextmanager + def _runtest_for(self, item, when): with self._runtest_for_main(item, when): if self.log_file_handler is not None: with catching_logs(self.log_file_handler, level=self.log_file_level): @@ -549,71 +549,71 @@ class LoggingPlugin(object): @contextmanager def _runtest_for_main(self, item, when): - """Implements the internals of pytest_runtest_xxx() hook.""" - with catching_logs( - LogCaptureHandler(), formatter=self.formatter, level=self.log_level - ) as log_handler: - if self.log_cli_handler: - self.log_cli_handler.set_when(when) - - if item is None: - yield # run the test - return - - if not hasattr(item, "catch_log_handlers"): - item.catch_log_handlers = {} - item.catch_log_handlers[when] = log_handler - item.catch_log_handler = log_handler - try: - yield # run test - finally: - if when == "teardown": - del item.catch_log_handler - del item.catch_log_handlers - - if self.print_logs: - # Add a captured log section to the report. - log = log_handler.stream.getvalue().strip() - item.add_report_section(when, "log", log) - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_setup(self, item): - with self._runtest_for(item, "setup"): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item): - with self._runtest_for(item, "call"): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_teardown(self, item): - with self._runtest_for(item, "teardown"): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_logstart(self): - if self.log_cli_handler: - self.log_cli_handler.reset() - with self._runtest_for(None, "start"): - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_logfinish(self): - with self._runtest_for(None, "finish"): - yield - + """Implements the internals of pytest_runtest_xxx() hook.""" + with catching_logs( + LogCaptureHandler(), formatter=self.formatter, level=self.log_level + ) as log_handler: + if self.log_cli_handler: + self.log_cli_handler.set_when(when) + + if item is None: + yield # run the test + return + + if not hasattr(item, "catch_log_handlers"): + item.catch_log_handlers = {} + item.catch_log_handlers[when] = log_handler + item.catch_log_handler = log_handler + try: + yield # run test + finally: + if when == "teardown": + del item.catch_log_handler + del item.catch_log_handlers + + if self.print_logs: + # Add a captured log section to the report. + log = log_handler.stream.getvalue().strip() + item.add_report_section(when, "log", log) + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_setup(self, item): + with self._runtest_for(item, "setup"): + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_call(self, item): + with self._runtest_for(item, "call"): + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_teardown(self, item): + with self._runtest_for(item, "teardown"): + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_logstart(self): + if self.log_cli_handler: + self.log_cli_handler.reset() + with self._runtest_for(None, "start"): + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_logfinish(self): + with self._runtest_for(None, "finish"): + yield + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_logreport(self): with self._runtest_for(None, "logreport"): yield - @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_sessionfinish(self): - with self.live_logs_context(): - if self.log_cli_handler: - self.log_cli_handler.set_when("sessionfinish") - if self.log_file_handler is not None: + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_sessionfinish(self): + with self.live_logs_context(): + if self.log_cli_handler: + self.log_cli_handler.set_when("sessionfinish") + if self.log_file_handler is not None: try: with catching_logs( self.log_file_handler, level=self.log_file_level @@ -623,23 +623,23 @@ class LoggingPlugin(object): # Close the FileHandler explicitly. # (logging.shutdown might have lost the weakref?!) self.log_file_handler.close() - else: - yield - - @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_sessionstart(self): - with self.live_logs_context(): - if self.log_cli_handler: - self.log_cli_handler.set_when("sessionstart") - if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield - else: - yield - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtestloop(self, session): - """Runs all collected test items.""" + else: + yield + + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_sessionstart(self): + with self.live_logs_context(): + if self.log_cli_handler: + self.log_cli_handler.set_when("sessionstart") + if self.log_file_handler is not None: + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield + else: + yield + + @pytest.hookimpl(hookwrapper=True) + def pytest_runtestloop(self, session): + """Runs all collected test items.""" if session.config.option.collectonly: yield @@ -649,60 +649,60 @@ class LoggingPlugin(object): # setting verbose flag is needed to avoid messy test progress output self._config.option.verbose = 1 - with self.live_logs_context(): - if self.log_file_handler is not None: - with catching_logs(self.log_file_handler, level=self.log_file_level): - yield # run all the tests - else: - yield # run all the tests - - -class _LiveLoggingStreamHandler(logging.StreamHandler): - """ - Custom 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. - """ - - def __init__(self, terminal_reporter, capture_manager): - """ - :param _pytest.terminal.TerminalReporter terminal_reporter: - :param _pytest.capture.CaptureManager capture_manager: - """ - logging.StreamHandler.__init__(self, stream=terminal_reporter) - self.capture_manager = capture_manager - self.reset() - self.set_when(None) - self._test_outcome_written = False - - def reset(self): - """Reset the handler; should be called before the start of each test""" - self._first_record_emitted = False - - def set_when(self, when): - """Prepares for the given test phase (setup/call/teardown)""" - self._when = when - self._section_name_shown = False - if when == "start": - self._test_outcome_written = False - - def emit(self, record): - ctx_manager = ( - self.capture_manager.global_and_fixture_disabled() - if self.capture_manager - else dummy_context_manager() - ) - 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 - logging.StreamHandler.emit(self, record) + with self.live_logs_context(): + if self.log_file_handler is not None: + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield # run all the tests + else: + yield # run all the tests + + +class _LiveLoggingStreamHandler(logging.StreamHandler): + """ + Custom 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. + """ + + def __init__(self, terminal_reporter, capture_manager): + """ + :param _pytest.terminal.TerminalReporter terminal_reporter: + :param _pytest.capture.CaptureManager capture_manager: + """ + logging.StreamHandler.__init__(self, stream=terminal_reporter) + self.capture_manager = capture_manager + self.reset() + self.set_when(None) + self._test_outcome_written = False + + def reset(self): + """Reset the handler; should be called before the start of each test""" + self._first_record_emitted = False + + def set_when(self, when): + """Prepares for the given test phase (setup/call/teardown)""" + self._when = when + self._section_name_shown = False + if when == "start": + self._test_outcome_written = False + + def emit(self, record): + ctx_manager = ( + self.capture_manager.global_and_fixture_disabled() + if self.capture_manager + else dummy_context_manager() + ) + 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 + logging.StreamHandler.emit(self, record) diff --git a/contrib/python/pytest/py2/_pytest/main.py b/contrib/python/pytest/py2/_pytest/main.py index 5bfa2e1ff1..a9d310cb62 100644 --- a/contrib/python/pytest/py2/_pytest/main.py +++ b/contrib/python/pytest/py2/_pytest/main.py @@ -1,177 +1,177 @@ # -*- coding: utf-8 -*- -""" core implementation of testing process: init, session, runtest loop. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import contextlib +""" core implementation of testing process: init, session, runtest loop. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import contextlib import fnmatch -import functools -import os -import pkgutil -import sys +import functools +import os +import pkgutil +import sys import warnings - -import attr -import py -import six - -import _pytest._code -from _pytest import nodes -from _pytest.config import directory_arg -from _pytest.config import hookimpl -from _pytest.config import UsageError + +import attr +import py +import six + +import _pytest._code +from _pytest import nodes +from _pytest.config import directory_arg +from _pytest.config import hookimpl +from _pytest.config import UsageError from _pytest.deprecated import PYTEST_CONFIG_GLOBAL -from _pytest.outcomes import exit -from _pytest.runner import collect_one_node - -# exitcodes for the command line -EXIT_OK = 0 -EXIT_TESTSFAILED = 1 -EXIT_INTERRUPTED = 2 -EXIT_INTERNALERROR = 3 -EXIT_USAGEERROR = 4 -EXIT_NOTESTSCOLLECTED = 5 - - -def pytest_addoption(parser): - parser.addini( - "norecursedirs", - "directory patterns to avoid for recursion", - type="args", - default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"], - ) - 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._addoption( - "--maxfail", - metavar="num", - action="store", - type=int, - dest="maxfail", - default=0, - help="exit after first num failures or errors.", - ) - group._addoption( +from _pytest.outcomes import exit +from _pytest.runner import collect_one_node + +# exitcodes for the command line +EXIT_OK = 0 +EXIT_TESTSFAILED = 1 +EXIT_INTERRUPTED = 2 +EXIT_INTERNALERROR = 3 +EXIT_USAGEERROR = 4 +EXIT_NOTESTSCOLLECTED = 5 + + +def pytest_addoption(parser): + parser.addini( + "norecursedirs", + "directory patterns to avoid for recursion", + type="args", + default=[".*", "build", "dist", "CVS", "_darcs", "{arch}", "*.egg", "venv"], + ) + 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._addoption( + "--maxfail", + metavar="num", + action="store", + type=int, + dest="maxfail", + default=0, + help="exit after first num failures or errors.", + ) + group._addoption( "--strict-markers", - "--strict", - action="store_true", + "--strict", + action="store_true", help="markers not registered in the `markers` section of the configuration file raise errors.", - ) - 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", - 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( + "-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", + 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( "--ignore-glob", action="append", metavar="path", help="ignore path pattern during collection (multi-allowed).", ) group.addoption( - "--deselect", - action="append", - metavar="nodeid_prefix", - help="deselect item during collection (multi-allowed).", - ) - # when changing this to --conf-cut-dir, config.py Conftest.setinitial - # needs upgrading as well - 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 = parser.getgroup("debugconfig", "test session debugging and configuration") - group.addoption( - "--basetemp", - dest="basetemp", - default=None, - metavar="dir", - help=( - "base temporary directory for this test run." - "(warning: this directory is removed if it exists)" - ), - ) - - + "--deselect", + action="append", + metavar="nodeid_prefix", + help="deselect item during collection (multi-allowed).", + ) + # when changing this to --conf-cut-dir, config.py Conftest.setinitial + # needs upgrading as well + 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 = parser.getgroup("debugconfig", "test session debugging and configuration") + group.addoption( + "--basetemp", + dest="basetemp", + default=None, + metavar="dir", + help=( + "base temporary directory for this test run." + "(warning: this directory is removed if it exists)" + ), + ) + + class _ConfigDeprecated(object): def __init__(self, config): self.__dict__["_config"] = config @@ -188,121 +188,121 @@ class _ConfigDeprecated(object): return "{}({!r})".format(type(self).__name__, self._config) -def pytest_configure(config): +def pytest_configure(config): __import__("pytest").config = _ConfigDeprecated(config) # compatibility - - -def wrap_session(config, doit): - """Skeleton command line program""" - session = Session(config) - session.exitstatus = EXIT_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: + + +def wrap_session(config, doit): + """Skeleton command line program""" + session = Session(config) + session.exitstatus = EXIT_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: session.exitstatus = EXIT_USAGEERROR - raise - except Failed: - session.exitstatus = EXIT_TESTSFAILED + raise + except Failed: + session.exitstatus = EXIT_TESTSFAILED except (KeyboardInterrupt, exit.Exception): excinfo = _pytest._code.ExceptionInfo.from_current() - exitstatus = EXIT_INTERRUPTED + exitstatus = EXIT_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( "{}: {}\n".format(excinfo.typename, excinfo.value.msg) ) - config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - session.exitstatus = exitstatus - except: # noqa + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = exitstatus + except: # noqa excinfo = _pytest._code.ExceptionInfo.from_current() - config.notify_exception(excinfo, config.option) - session.exitstatus = EXIT_INTERNALERROR - if excinfo.errisinstance(SystemExit): + config.notify_exception(excinfo, config.option) + session.exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): sys.stderr.write("mainloop: caught unexpected SystemExit!\n") - - finally: - excinfo = None # Explicitly break reference cycle. - session.startdir.chdir() - if initstate >= 2: - config.hook.pytest_sessionfinish( - session=session, exitstatus=session.exitstatus - ) - config._ensure_unconfigure() - return session.exitstatus - - -def pytest_cmdline_main(config): - return wrap_session(config, _main) - - -def _main(config, session): - """ 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: - return EXIT_TESTSFAILED - elif session.testscollected == 0: - return EXIT_NOTESTSCOLLECTED - - -def pytest_collection(session): - return session.perform_collect() - - -def pytest_runtestloop(session): - if session.testsfailed and not session.config.option.continue_on_collection_errors: - raise session.Interrupted("%d errors during collection" % session.testsfailed) - - 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): - """Attempts 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()]) - - -def pytest_ignore_collect(path, config): - 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 - + + finally: + excinfo = None # Explicitly break reference cycle. + session.startdir.chdir() + if initstate >= 2: + config.hook.pytest_sessionfinish( + session=session, exitstatus=session.exitstatus + ) + config._ensure_unconfigure() + return session.exitstatus + + +def pytest_cmdline_main(config): + return wrap_session(config, _main) + + +def _main(config, session): + """ 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: + return EXIT_TESTSFAILED + elif session.testscollected == 0: + return EXIT_NOTESTSCOLLECTED + + +def pytest_collection(session): + return session.perform_collect() + + +def pytest_runtestloop(session): + if session.testsfailed and not session.config.option.continue_on_collection_errors: + raise session.Interrupted("%d errors during collection" % session.testsfailed) + + 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): + """Attempts 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()]) + + +def pytest_ignore_collect(path, config): + 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() ) @@ -317,131 +317,131 @@ def pytest_ignore_collect(path, config): ): return True - allow_in_venv = config.getoption("collect_in_virtualenv") - if not allow_in_venv and _in_venv(path): - return True - - return False - - -def pytest_collection_modifyitems(items, config): - 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 - - -@contextlib.contextmanager -def _patched_find_module(): - """Patch bug in pkgutil.ImpImporter.find_module - - When using pkgutil.find_loader on python<3.4 it removes symlinks - from the path due to a call to os.path.realpath. This is not consistent - with actually doing the import (in these versions, pkgutil and __import__ - did not share the same underlying code). This can break conftest - discovery for pytest where symlinks are involved. - - The only supported python<3.4 by pytest is python 2.7. - """ - if six.PY2: # python 3.4+ uses importlib instead - - def find_module_patched(self, fullname, path=None): - # Note: we ignore 'path' argument since it is only used via meta_path - subname = fullname.split(".")[-1] - if subname != fullname and self.path is None: - return None - if self.path is None: - path = None - else: - # original: path = [os.path.realpath(self.path)] - path = [self.path] - try: - file, filename, etc = pkgutil.imp.find_module(subname, path) - except ImportError: - return None - return pkgutil.ImpLoader(fullname, file, filename, etc) - - old_find_module = pkgutil.ImpImporter.find_module - pkgutil.ImpImporter.find_module = find_module_patched - try: - yield - finally: - pkgutil.ImpImporter.find_module = old_find_module - else: - yield - - -class FSHookProxy(object): - def __init__(self, fspath, pm, remove_mods): - self.fspath = fspath - self.pm = pm - self.remove_mods = remove_mods - - def __getattr__(self, name): - x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) - self.__dict__[name] = x - return x - - -class NoMatch(Exception): - """ raised if matching cannot locate a matching names. """ - - -class Interrupted(KeyboardInterrupt): - """ signals an interrupted test run. """ - - __module__ = "builtins" # for py3 - - -class Failed(Exception): - """ signals a stop as failed test run. """ - - -@attr.s -class _bestrelpath_cache(dict): - path = attr.ib() - - def __missing__(self, path): - r = self.path.bestrelpath(path) - self[path] = r - return r - - -class Session(nodes.FSCollector): - Interrupted = Interrupted - Failed = Failed - - def __init__(self, config): - nodes.FSCollector.__init__( - self, config.rootdir, parent=None, config=config, session=self, nodeid="" - ) - self.testsfailed = 0 - self.testscollected = 0 - self.shouldstop = False - self.shouldfail = False - self.trace = config.trace.root.get("collection") - self._norecursepatterns = config.getini("norecursedirs") + allow_in_venv = config.getoption("collect_in_virtualenv") + if not allow_in_venv and _in_venv(path): + return True + + return False + + +def pytest_collection_modifyitems(items, config): + 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 + + +@contextlib.contextmanager +def _patched_find_module(): + """Patch bug in pkgutil.ImpImporter.find_module + + When using pkgutil.find_loader on python<3.4 it removes symlinks + from the path due to a call to os.path.realpath. This is not consistent + with actually doing the import (in these versions, pkgutil and __import__ + did not share the same underlying code). This can break conftest + discovery for pytest where symlinks are involved. + + The only supported python<3.4 by pytest is python 2.7. + """ + if six.PY2: # python 3.4+ uses importlib instead + + def find_module_patched(self, fullname, path=None): + # Note: we ignore 'path' argument since it is only used via meta_path + subname = fullname.split(".")[-1] + if subname != fullname and self.path is None: + return None + if self.path is None: + path = None + else: + # original: path = [os.path.realpath(self.path)] + path = [self.path] + try: + file, filename, etc = pkgutil.imp.find_module(subname, path) + except ImportError: + return None + return pkgutil.ImpLoader(fullname, file, filename, etc) + + old_find_module = pkgutil.ImpImporter.find_module + pkgutil.ImpImporter.find_module = find_module_patched + try: + yield + finally: + pkgutil.ImpImporter.find_module = old_find_module + else: + yield + + +class FSHookProxy(object): + def __init__(self, fspath, pm, remove_mods): + self.fspath = fspath + self.pm = pm + self.remove_mods = remove_mods + + def __getattr__(self, name): + x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods) + self.__dict__[name] = x + return x + + +class NoMatch(Exception): + """ raised if matching cannot locate a matching names. """ + + +class Interrupted(KeyboardInterrupt): + """ signals an interrupted test run. """ + + __module__ = "builtins" # for py3 + + +class Failed(Exception): + """ signals a stop as failed test run. """ + + +@attr.s +class _bestrelpath_cache(dict): + path = attr.ib() + + def __missing__(self, path): + r = self.path.bestrelpath(path) + self[path] = r + return r + + +class Session(nodes.FSCollector): + Interrupted = Interrupted + Failed = Failed + + def __init__(self, config): + nodes.FSCollector.__init__( + self, config.rootdir, parent=None, config=config, session=self, nodeid="" + ) + self.testsfailed = 0 + self.testscollected = 0 + self.shouldstop = False + self.shouldfail = False + self.trace = config.trace.root.get("collection") + self._norecursepatterns = config.getini("norecursedirs") self.startdir = config.invocation_dir - self._initialpaths = frozenset() - # Keep track of any collected nodes in here, so we don't duplicate fixtures - self._node_cache = {} - self._bestrelpathcache = _bestrelpath_cache(config.rootdir) - # Dirnames of pkgs with dunder-init files. - self._pkg_roots = {} - - self.config.pluginmanager.register(self, name="session") - + self._initialpaths = frozenset() + # Keep track of any collected nodes in here, so we don't duplicate fixtures + self._node_cache = {} + self._bestrelpathcache = _bestrelpath_cache(config.rootdir) + # Dirnames of pkgs with dunder-init files. + self._pkg_roots = {} + + self.config.pluginmanager.register(self, name="session") + def __repr__(self): return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( self.__class__.__name__, @@ -451,176 +451,176 @@ class Session(nodes.FSCollector): self.testscollected, ) - def _node_location_to_relpath(self, node_path): - # bestrelpath is a quite slow function - return self._bestrelpathcache[node_path] - - @hookimpl(tryfirst=True) - def pytest_collectstart(self): - 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): - 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): - return path in self._initialpaths - - def gethookproxy(self, fspath): - # check if we have the common case of running - # hooks with all conftest.py files - pm = self.config.pluginmanager - my_conftestmodules = pm._getconftestmodules(fspath) - remove_mods = pm._conftest_plugins.difference(my_conftestmodules) - if remove_mods: - # one or more conftests are not in use at this fspath - proxy = FSHookProxy(fspath, pm, remove_mods) - else: - # all plugis are active for this fspath - proxy = self.config.hook - return proxy - - def perform_collect(self, args=None, genitems=True): - hook = self.config.hook - try: - items = self._perform_collect(args, genitems) - 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 - - def _perform_collect(self, args, genitems): - if args is None: - args = self.config.args - self.trace("perform_collect", self, args) - self.trace.root.indent += 1 - self._notfound = [] - initialpaths = [] - self._initialparts = [] - self.items = items = [] - for arg in args: - parts = self._parsearg(arg) - self._initialparts.append(parts) - initialpaths.append(parts[0]) - self._initialpaths = frozenset(initialpaths) - rep = collect_one_node(self) - self.ihook.pytest_collectreport(report=rep) - self.trace.root.indent -= 1 - if self._notfound: - errors = [] - for arg, exc in self._notfound: - line = "(no name %r in any of %r)" % (arg, exc.args[0]) - errors.append("not found: %s\n%s" % (arg, line)) - # XXX: test this - raise UsageError(*errors) - if not genitems: - return rep.result - else: - if rep.passed: - for node in rep.result: - self.items.extend(self.genitems(node)) - return items - - def collect(self): - for initialpart in self._initialparts: - arg = "::".join(map(str, initialpart)) - self.trace("processing argument", arg) - self.trace.root.indent += 1 - try: - for x in self._collect(arg): - yield x - except NoMatch: - # we are inside a make_report hook so - # we cannot directly pass through the exception - self._notfound.append((arg, sys.exc_info()[1])) - - self.trace.root.indent -= 1 - - def _collect(self, arg): - from _pytest.python import Package - - names = self._parsearg(arg) - argpath = names.pop(0) - - # 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 + def _node_location_to_relpath(self, node_path): + # bestrelpath is a quite slow function + return self._bestrelpathcache[node_path] + + @hookimpl(tryfirst=True) + def pytest_collectstart(self): + 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): + 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): + return path in self._initialpaths + + def gethookproxy(self, fspath): + # check if we have the common case of running + # hooks with all conftest.py files + pm = self.config.pluginmanager + my_conftestmodules = pm._getconftestmodules(fspath) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + if remove_mods: + # one or more conftests are not in use at this fspath + proxy = FSHookProxy(fspath, pm, remove_mods) + else: + # all plugis are active for this fspath + proxy = self.config.hook + return proxy + + def perform_collect(self, args=None, genitems=True): + hook = self.config.hook + try: + items = self._perform_collect(args, genitems) + 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 + + def _perform_collect(self, args, genitems): + if args is None: + args = self.config.args + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + self._notfound = [] + initialpaths = [] + self._initialparts = [] + self.items = items = [] + for arg in args: + parts = self._parsearg(arg) + self._initialparts.append(parts) + initialpaths.append(parts[0]) + self._initialpaths = frozenset(initialpaths) + rep = collect_one_node(self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + errors = [] + for arg, exc in self._notfound: + line = "(no name %r in any of %r)" % (arg, exc.args[0]) + errors.append("not found: %s\n%s" % (arg, line)) + # XXX: test this + raise UsageError(*errors) + if not genitems: + return rep.result + else: + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + return items + + def collect(self): + for initialpart in self._initialparts: + arg = "::".join(map(str, initialpart)) + self.trace("processing argument", arg) + self.trace.root.indent += 1 + try: + for x in self._collect(arg): + yield x + except NoMatch: + # we are inside a make_report hook so + # we cannot directly pass through the exception + self._notfound.append((arg, sys.exc_info()[1])) + + self.trace.root.indent -= 1 + + def _collect(self, arg): + from _pytest.python import Package + + names = self._parsearg(arg) + argpath = names.pop(0) + + # 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 if not self.config.getoption("doctestmodules", False): - pm = self.config.pluginmanager - for parent in reversed(argpath.parts()): - if pm._confcutdir and pm._confcutdir.relto(parent): - break - - if parent.isdir(): - pkginit = parent.join("__init__.py") - if pkginit.isfile(): - if pkginit not in self._node_cache: - col = self._collectfile(pkginit, handle_dupes=False) - if col: - if isinstance(col[0], Package): - self._pkg_roots[parent] = col[0] - # always store a list in the cache, matchnodes expects it - self._node_cache[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" % (arg,) - - seen_dirs = set() - for path in argpath.visit( + pm = self.config.pluginmanager + for parent in reversed(argpath.parts()): + if pm._confcutdir and pm._confcutdir.relto(parent): + break + + if parent.isdir(): + pkginit = parent.join("__init__.py") + if pkginit.isfile(): + if pkginit not in self._node_cache: + col = self._collectfile(pkginit, handle_dupes=False) + if col: + if isinstance(col[0], Package): + self._pkg_roots[parent] = col[0] + # always store a list in the cache, matchnodes expects it + self._node_cache[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" % (arg,) + + seen_dirs = set() + for path in argpath.visit( fil=self._visit_filter, rec=self._recurse, bf=True, sort=True - ): - dirpath = path.dirpath() - if dirpath not in seen_dirs: - # Collect packages first. - seen_dirs.add(dirpath) - pkginit = dirpath.join("__init__.py") - if pkginit.exists(): - for x in self._collectfile(pkginit): - yield x - if isinstance(x, Package): - self._pkg_roots[dirpath] = x - if dirpath in self._pkg_roots: - # Do not collect packages here. - continue - - for x in self._collectfile(path): - key = (type(x), x.fspath) - if key in self._node_cache: - yield self._node_cache[key] - else: - self._node_cache[key] = x - yield x - else: - assert argpath.check(file=1) - - if argpath in self._node_cache: - col = self._node_cache[argpath] - else: - collect_root = self._pkg_roots.get(argpath.dirname, self) + ): + dirpath = path.dirpath() + if dirpath not in seen_dirs: + # Collect packages first. + seen_dirs.add(dirpath) + pkginit = dirpath.join("__init__.py") + if pkginit.exists(): + for x in self._collectfile(pkginit): + yield x + if isinstance(x, Package): + self._pkg_roots[dirpath] = x + if dirpath in self._pkg_roots: + # Do not collect packages here. + continue + + for x in self._collectfile(path): + key = (type(x), x.fspath) + if key in self._node_cache: + yield self._node_cache[key] + else: + self._node_cache[key] = x + yield x + else: + assert argpath.check(file=1) + + if argpath in self._node_cache: + col = self._node_cache[argpath] + else: + collect_root = self._pkg_roots.get(argpath.dirname, self) col = collect_root._collectfile(argpath, handle_dupes=False) - if col: - self._node_cache[argpath] = col - m = self.matchnodes(col, names) - # If __init__.py was the only file requested, then the matched node will be - # the corresponding Package, and the first yielded item will be the __init__ - # Module itself, so just use that. If this special case isn't taken, then all - # the files in the package will be yielded. - if argpath.basename == "__init__.py": + if col: + self._node_cache[argpath] = col + m = self.matchnodes(col, names) + # If __init__.py was the only file requested, then the matched node will be + # the corresponding Package, and the first yielded item will be the __init__ + # Module itself, so just use that. If this special case isn't taken, then all + # the files in the package will be yielded. + if argpath.basename == "__init__.py": try: yield next(m[0].collect()) except StopIteration: @@ -628,46 +628,46 @@ class Session(nodes.FSCollector): # file in it, which gets ignored by the default # "python_files" option. pass - return - for y in m: - yield y - - def _collectfile(self, path, handle_dupes=True): + return + for y in m: + yield y + + def _collectfile(self, path, handle_dupes=True): assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % ( path, path.isdir(), path.exists(), path.islink(), ) - ihook = self.gethookproxy(path) - if not self.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): - return () - - if handle_dupes: - keepduplicates = self.config.getoption("keepduplicates") - if not keepduplicates: - duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: - return () - else: - duplicate_paths.add(path) - - return ihook.pytest_collect_file(path=path, parent=self) - - def _recurse(self, dirpath): - if dirpath.basename == "__pycache__": - return False - ihook = self.gethookproxy(dirpath.dirpath()) - if ihook.pytest_ignore_collect(path=dirpath, config=self.config): - return False - for pat in self._norecursepatterns: - if dirpath.check(fnmatch=pat): - return False - ihook = self.gethookproxy(dirpath) - ihook.pytest_collect_directory(path=dirpath, parent=self) - return True - + ihook = self.gethookproxy(path) + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + + if handle_dupes: + keepduplicates = self.config.getoption("keepduplicates") + if not keepduplicates: + duplicate_paths = self.config.pluginmanager._duplicatepaths + if path in duplicate_paths: + return () + else: + duplicate_paths.add(path) + + return ihook.pytest_collect_file(path=path, parent=self) + + def _recurse(self, dirpath): + if dirpath.basename == "__pycache__": + return False + ihook = self.gethookproxy(dirpath.dirpath()) + if ihook.pytest_ignore_collect(path=dirpath, config=self.config): + return False + for pat in self._norecursepatterns: + if dirpath.check(fnmatch=pat): + return False + ihook = self.gethookproxy(dirpath) + ihook.pytest_collect_directory(path=dirpath, parent=self) + return True + if six.PY2: @staticmethod @@ -680,101 +680,101 @@ class Session(nodes.FSCollector): def _visit_filter(f): return f.check(file=1) - def _tryconvertpyarg(self, x): - """Convert a dotted module name to path.""" - try: - with _patched_find_module(): - loader = pkgutil.find_loader(x) - except ImportError: - return x - if loader is None: - return x - # This method is sometimes invoked when AssertionRewritingHook, which - # does not define a get_filename method, is already in place: - try: - with _patched_find_module(): - path = loader.get_filename(x) - except AttributeError: - # Retrieve path from AssertionRewritingHook: - path = loader.modules[x][0].co_filename - if loader.is_package(x): - path = os.path.dirname(path) - return path - - def _parsearg(self, arg): - """ return (fspath, names) tuple after checking the file exists. """ - parts = str(arg).split("::") - if self.config.option.pyargs: - parts[0] = self._tryconvertpyarg(parts[0]) - relpath = parts[0].replace("/", os.sep) - path = self.config.invocation_dir.join(relpath, abs=True) - if not path.check(): - if self.config.option.pyargs: - raise UsageError( - "file or package not found: " + arg + " (missing __init__.py?)" - ) - raise UsageError("file not found: " + arg) - parts[0] = path.realpath() - return parts - - def matchnodes(self, matching, names): - self.trace("matchnodes", matching, names) - self.trace.root.indent += 1 - nodes = self._matchnodes(matching, names) - num = len(nodes) - self.trace("matchnodes finished -> ", num, "nodes") - self.trace.root.indent -= 1 - if num == 0: - raise NoMatch(matching, names[:1]) - return nodes - - def _matchnodes(self, matching, names): - if not matching or not names: - return matching - name = names[0] - assert name - nextnames = names[1:] - resultnodes = [] - for node in matching: - if isinstance(node, nodes.Item): - if not names: - resultnodes.append(node) - continue - assert isinstance(node, nodes.Collector) - key = (type(node), node.nodeid) - if key in self._node_cache: - rep = self._node_cache[key] - else: - rep = collect_one_node(node) - self._node_cache[key] = rep - if rep.passed: - has_matched = False - for x in rep.result: - # TODO: remove parametrized workaround once collection structure contains parametrization - if x.name == name or x.name.split("[")[0] == name: - resultnodes.extend(self.matchnodes([x], nextnames)) - has_matched = True - # XXX accept IDs that don't have "()" for class instances - if not has_matched and len(rep.result) == 1 and x.name == "()": - nextnames.insert(0, name) - resultnodes.extend(self.matchnodes([x], nextnames)) - else: - # report collection failures here to avoid failing to run some test - # specified in the command line because the module could not be - # imported (#134) - node.ihook.pytest_collectreport(report=rep) - return resultnodes - - def genitems(self, node): - 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: - for x in self.genitems(subnode): - yield x - node.ihook.pytest_collectreport(report=rep) + def _tryconvertpyarg(self, x): + """Convert a dotted module name to path.""" + try: + with _patched_find_module(): + loader = pkgutil.find_loader(x) + except ImportError: + return x + if loader is None: + return x + # This method is sometimes invoked when AssertionRewritingHook, which + # does not define a get_filename method, is already in place: + try: + with _patched_find_module(): + path = loader.get_filename(x) + except AttributeError: + # Retrieve path from AssertionRewritingHook: + path = loader.modules[x][0].co_filename + if loader.is_package(x): + path = os.path.dirname(path) + return path + + def _parsearg(self, arg): + """ return (fspath, names) tuple after checking the file exists. """ + parts = str(arg).split("::") + if self.config.option.pyargs: + parts[0] = self._tryconvertpyarg(parts[0]) + relpath = parts[0].replace("/", os.sep) + path = self.config.invocation_dir.join(relpath, abs=True) + if not path.check(): + if self.config.option.pyargs: + raise UsageError( + "file or package not found: " + arg + " (missing __init__.py?)" + ) + raise UsageError("file not found: " + arg) + parts[0] = path.realpath() + return parts + + def matchnodes(self, matching, names): + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 + nodes = self._matchnodes(matching, names) + num = len(nodes) + self.trace("matchnodes finished -> ", num, "nodes") + self.trace.root.indent -= 1 + if num == 0: + raise NoMatch(matching, names[:1]) + return nodes + + def _matchnodes(self, matching, names): + if not matching or not names: + return matching + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] + for node in matching: + if isinstance(node, nodes.Item): + if not names: + resultnodes.append(node) + continue + assert isinstance(node, nodes.Collector) + key = (type(node), node.nodeid) + if key in self._node_cache: + rep = self._node_cache[key] + else: + rep = collect_one_node(node) + self._node_cache[key] = rep + if rep.passed: + has_matched = False + for x in rep.result: + # TODO: remove parametrized workaround once collection structure contains parametrization + if x.name == name or x.name.split("[")[0] == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + has_matched = True + # XXX accept IDs that don't have "()" for class instances + if not has_matched and len(rep.result) == 1 and x.name == "()": + nextnames.insert(0, name) + resultnodes.extend(self.matchnodes([x], nextnames)) + else: + # report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134) + node.ihook.pytest_collectreport(report=rep) + return resultnodes + + def genitems(self, node): + 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: + for x in self.genitems(subnode): + yield x + node.ihook.pytest_collectreport(report=rep) diff --git a/contrib/python/pytest/py2/_pytest/mark/__init__.py b/contrib/python/pytest/py2/_pytest/mark/__init__.py index fc8ceb57ba..6bc22fe27d 100644 --- a/contrib/python/pytest/py2/_pytest/mark/__init__.py +++ b/contrib/python/pytest/py2/_pytest/mark/__init__.py @@ -1,166 +1,166 @@ # -*- coding: utf-8 -*- -""" generic mechanism for marking and selecting python functions. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from .legacy import matchkeyword -from .legacy import matchmark -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 UsageError - +""" generic mechanism for marking and selecting python functions. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from .legacy import matchkeyword +from .legacy import matchmark +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 UsageError + __all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] - - -def param(*values, **kw): - """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 - - :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, **kw) - - -def pytest_addoption(parser): - 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. " + + +def param(*values, **kw): + """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 + + :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, **kw) + + +def pytest_addoption(parser): + 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, " - "as well as functions which have names assigned directly to them.", - ) - - group._addoption( - "-m", - action="store", - dest="markexpr", - default="", - metavar="MARKEXPR", - help="only run tests matching given mark expression. " - "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") - - -def pytest_cmdline_main(config): - 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 - - -pytest_cmdline_main.tryfirst = True - - -def deselect_by_keyword(items, config): - keywordexpr = config.option.keyword.lstrip() + "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.", + ) + + group._addoption( + "-m", + action="store", + dest="markexpr", + default="", + metavar="MARKEXPR", + help="only run tests matching given mark expression. " + "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") + + +def pytest_cmdline_main(config): + 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 + + +pytest_cmdline_main.tryfirst = True + + +def deselect_by_keyword(items, config): + keywordexpr = config.option.keyword.lstrip() if not keywordexpr: return - if keywordexpr.startswith("-"): - keywordexpr = "not " + keywordexpr[1:] - selectuntil = False - if keywordexpr[-1:] == ":": - selectuntil = True - keywordexpr = keywordexpr[:-1] - - remaining = [] - deselected = [] - for colitem in items: - if keywordexpr and not matchkeyword(colitem, keywordexpr): - deselected.append(colitem) - else: - if selectuntil: - keywordexpr = None - remaining.append(colitem) - - if deselected: - config.hook.pytest_deselected(items=deselected) - items[:] = remaining - - -def deselect_by_mark(items, config): - matchexpr = config.option.markexpr - if not matchexpr: - return - - remaining = [] - deselected = [] - for item in items: - if matchmark(item, matchexpr): - remaining.append(item) - else: - deselected.append(item) - - if deselected: - config.hook.pytest_deselected(items=deselected) - items[:] = remaining - - -def pytest_collection_modifyitems(items, config): - deselect_by_keyword(items, config) - deselect_by_mark(items, config) - - -def pytest_configure(config): - config._old_mark_config = MARK_GEN._config + if keywordexpr.startswith("-"): + keywordexpr = "not " + keywordexpr[1:] + selectuntil = False + if keywordexpr[-1:] == ":": + selectuntil = True + keywordexpr = keywordexpr[:-1] + + remaining = [] + deselected = [] + for colitem in items: + if keywordexpr and not matchkeyword(colitem, keywordexpr): + deselected.append(colitem) + else: + if selectuntil: + keywordexpr = None + remaining.append(colitem) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +def deselect_by_mark(items, config): + matchexpr = config.option.markexpr + if not matchexpr: + return + + remaining = [] + deselected = [] + for item in items: + if matchmark(item, matchexpr): + remaining.append(item) + else: + deselected.append(item) + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + + +def pytest_collection_modifyitems(items, config): + deselect_by_keyword(items, config) + deselect_by_mark(items, config) + + +def pytest_configure(config): + config._old_mark_config = 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) - ) - - -def pytest_unconfigure(config): - MARK_GEN._config = getattr(config, "_old_mark_config", None) + + 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): + MARK_GEN._config = getattr(config, "_old_mark_config", None) diff --git a/contrib/python/pytest/py2/_pytest/mark/evaluate.py b/contrib/python/pytest/py2/_pytest/mark/evaluate.py index a6ce2aad3e..506546e253 100644 --- a/contrib/python/pytest/py2/_pytest/mark/evaluate.py +++ b/contrib/python/pytest/py2/_pytest/mark/evaluate.py @@ -1,126 +1,126 @@ # -*- coding: utf-8 -*- -import os -import platform -import sys -import traceback - -import six - -from ..outcomes import fail -from ..outcomes import TEST_OUTCOME - - -def cached_eval(config, expr, d): - if not hasattr(config, "_evalcache"): - config._evalcache = {} - try: - return config._evalcache[expr] - except KeyError: - import _pytest._code - - exprcode = _pytest._code.compile(expr, mode="eval") - config._evalcache[expr] = x = eval(exprcode, d) - return x - - -class MarkEvaluator(object): - def __init__(self, item, name): - self.item = item - self._marks = None - self._mark = None - self._mark_name = name - - def __bool__(self): - # dont cache here to prevent staleness - return bool(self._get_marks()) - - __nonzero__ = __bool__ - - def wasvalid(self): - return not hasattr(self, "exc") - - def _get_marks(self): - return list(self.item.iter_markers(name=self._mark_name)) - - def invalidraise(self, exc): - raises = self.get("raises") - if not raises: - return - return not isinstance(exc, raises) - - def istrue(self): - try: - return self._istrue() - except TEST_OUTCOME: - self.exc = sys.exc_info() - if isinstance(self.exc[1], SyntaxError): - msg = [" " * (self.exc[1].offset + 4) + "^"] - msg.append("SyntaxError: invalid syntax") - else: - msg = traceback.format_exception_only(*self.exc[:2]) - fail( - "Error evaluating %r expression\n" - " %s\n" - "%s" % (self._mark_name, self.expr, "\n".join(msg)), - pytrace=False, - ) - - def _getglobals(self): - d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} - if hasattr(self.item, "obj"): - d.update(self.item.obj.__globals__) - return d - - def _istrue(self): - if hasattr(self, "result"): - return self.result - self._marks = self._get_marks() - - if self._marks: - self.result = False - for mark in self._marks: - self._mark = mark - if "condition" in mark.kwargs: - args = (mark.kwargs["condition"],) - else: - args = mark.args - - for expr in args: - self.expr = expr - if isinstance(expr, six.string_types): - d = self._getglobals() - result = cached_eval(self.item.config, expr, d) - else: - if "reason" not in mark.kwargs: - # XXX better be checked at collection time - msg = ( - "you need to specify reason=STRING " - "when using booleans as conditions." - ) - fail(msg) - result = bool(expr) - if result: - self.result = True - self.reason = mark.kwargs.get("reason", None) - self.expr = expr - return self.result - - if not args: - self.result = True - self.reason = mark.kwargs.get("reason", None) - return self.result - return False - - def get(self, attr, default=None): - if self._mark is None: - return default - return self._mark.kwargs.get(attr, default) - - def getexplanation(self): - expl = getattr(self, "reason", None) or self.get("reason", None) - if not expl: - if not hasattr(self, "expr"): - return "" - else: - return "condition: " + str(self.expr) - return expl +import os +import platform +import sys +import traceback + +import six + +from ..outcomes import fail +from ..outcomes import TEST_OUTCOME + + +def cached_eval(config, expr, d): + if not hasattr(config, "_evalcache"): + config._evalcache = {} + try: + return config._evalcache[expr] + except KeyError: + import _pytest._code + + exprcode = _pytest._code.compile(expr, mode="eval") + config._evalcache[expr] = x = eval(exprcode, d) + return x + + +class MarkEvaluator(object): + def __init__(self, item, name): + self.item = item + self._marks = None + self._mark = None + self._mark_name = name + + def __bool__(self): + # dont cache here to prevent staleness + return bool(self._get_marks()) + + __nonzero__ = __bool__ + + def wasvalid(self): + return not hasattr(self, "exc") + + def _get_marks(self): + return list(self.item.iter_markers(name=self._mark_name)) + + def invalidraise(self, exc): + raises = self.get("raises") + if not raises: + return + return not isinstance(exc, raises) + + def istrue(self): + try: + return self._istrue() + except TEST_OUTCOME: + self.exc = sys.exc_info() + if isinstance(self.exc[1], SyntaxError): + msg = [" " * (self.exc[1].offset + 4) + "^"] + msg.append("SyntaxError: invalid syntax") + else: + msg = traceback.format_exception_only(*self.exc[:2]) + fail( + "Error evaluating %r expression\n" + " %s\n" + "%s" % (self._mark_name, self.expr, "\n".join(msg)), + pytrace=False, + ) + + def _getglobals(self): + d = {"os": os, "sys": sys, "platform": platform, "config": self.item.config} + if hasattr(self.item, "obj"): + d.update(self.item.obj.__globals__) + return d + + def _istrue(self): + if hasattr(self, "result"): + return self.result + self._marks = self._get_marks() + + if self._marks: + self.result = False + for mark in self._marks: + self._mark = mark + if "condition" in mark.kwargs: + args = (mark.kwargs["condition"],) + else: + args = mark.args + + for expr in args: + self.expr = expr + if isinstance(expr, six.string_types): + d = self._getglobals() + result = cached_eval(self.item.config, expr, d) + else: + if "reason" not in mark.kwargs: + # XXX better be checked at collection time + msg = ( + "you need to specify reason=STRING " + "when using booleans as conditions." + ) + fail(msg) + result = bool(expr) + if result: + self.result = True + self.reason = mark.kwargs.get("reason", None) + self.expr = expr + return self.result + + if not args: + self.result = True + self.reason = mark.kwargs.get("reason", None) + return self.result + return False + + def get(self, attr, default=None): + if self._mark is None: + return default + return self._mark.kwargs.get(attr, default) + + def getexplanation(self): + expl = getattr(self, "reason", None) or self.get("reason", None) + if not expl: + if not hasattr(self, "expr"): + return "" + else: + return "condition: " + str(self.expr) + return expl diff --git a/contrib/python/pytest/py2/_pytest/mark/legacy.py b/contrib/python/pytest/py2/_pytest/mark/legacy.py index 86dab370e9..c56482f14d 100644 --- a/contrib/python/pytest/py2/_pytest/mark/legacy.py +++ b/contrib/python/pytest/py2/_pytest/mark/legacy.py @@ -1,103 +1,103 @@ # -*- coding: utf-8 -*- -""" -this is a place where we put datastructures used by legacy apis -we hope ot remove -""" -import keyword - -import attr - -from _pytest.config import UsageError - - -@attr.s -class MarkMapping(object): - """Provides a local mapping for markers where item access - resolves to True if the marker is present. """ - - own_mark_names = attr.ib() - - @classmethod - def from_item(cls, item): - mark_names = {mark.name for mark in item.iter_markers()} - return cls(mark_names) - - def __getitem__(self, name): - return name in self.own_mark_names - - -class KeywordMapping(object): - """Provides a local mapping for keywords. - Given a list of names, map any substring of one of these names to True. - """ - - def __init__(self, names): - self._names = names - - @classmethod - def from_item(cls, item): - mapped_names = set() - - # Add the names of the current item and any parent items - import pytest - - for item in item.listchain(): - if not isinstance(item, pytest.Instance): - mapped_names.add(item.name) - - # Add the names added as extra keywords to current or parent items +""" +this is a place where we put datastructures used by legacy apis +we hope ot remove +""" +import keyword + +import attr + +from _pytest.config import UsageError + + +@attr.s +class MarkMapping(object): + """Provides a local mapping for markers where item access + resolves to True if the marker is present. """ + + own_mark_names = attr.ib() + + @classmethod + def from_item(cls, item): + mark_names = {mark.name for mark in item.iter_markers()} + return cls(mark_names) + + def __getitem__(self, name): + return name in self.own_mark_names + + +class KeywordMapping(object): + """Provides a local mapping for keywords. + Given a list of names, map any substring of one of these names to True. + """ + + def __init__(self, names): + self._names = names + + @classmethod + def from_item(cls, item): + mapped_names = set() + + # Add the names of the current item and any parent items + import pytest + + for item in item.listchain(): + if not isinstance(item, pytest.Instance): + mapped_names.add(item.name) + + # Add the names added as extra keywords to current or parent items mapped_names.update(item.listextrakeywords()) - - # Add the names attached to the current function through direct assignment - if hasattr(item, "function"): + + # Add the names attached to the current function through direct assignment + if hasattr(item, "function"): mapped_names.update(item.function.__dict__) - + # add the markers to the keywords as we no longer handle them correctly mapped_names.update(mark.name for mark in item.iter_markers()) - return cls(mapped_names) - - def __getitem__(self, subname): - for name in self._names: - if subname in name: - return True - return False - - -python_keywords_allowed_list = ["or", "and", "not"] - - -def matchmark(colitem, markexpr): - """Tries to match on any marker names, attached to the given colitem.""" - try: - return eval(markexpr, {}, MarkMapping.from_item(colitem)) - except SyntaxError as e: - raise SyntaxError(str(e) + "\nMarker expression must be valid Python!") - - -def matchkeyword(colitem, keywordexpr): - """Tries to match given keyword expression to given collector item. - - Will match on the name of colitem, including the names of its parents. - Only matches names of items which are either a :class:`Class` or a - :class:`Function`. - Additionally, matches on names in the 'extra_keyword_matches' set of - any item, as well as names directly assigned to test functions. - """ - mapping = KeywordMapping.from_item(colitem) - if " " not in keywordexpr: - # special case to allow for simple "-k pass" and "-k 1.3" - return mapping[keywordexpr] - elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]: - return not mapping[keywordexpr[4:]] - for kwd in keywordexpr.split(): - if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list: - raise UsageError( - "Python keyword '{}' not accepted in expressions passed to '-k'".format( - kwd - ) - ) - try: - return eval(keywordexpr, {}, mapping) - except SyntaxError: - raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr)) + return cls(mapped_names) + + def __getitem__(self, subname): + for name in self._names: + if subname in name: + return True + return False + + +python_keywords_allowed_list = ["or", "and", "not"] + + +def matchmark(colitem, markexpr): + """Tries to match on any marker names, attached to the given colitem.""" + try: + return eval(markexpr, {}, MarkMapping.from_item(colitem)) + except SyntaxError as e: + raise SyntaxError(str(e) + "\nMarker expression must be valid Python!") + + +def matchkeyword(colitem, keywordexpr): + """Tries to match given keyword expression to given collector item. + + Will match on the name of colitem, including the names of its parents. + Only matches names of items which are either a :class:`Class` or a + :class:`Function`. + Additionally, matches on names in the 'extra_keyword_matches' set of + any item, as well as names directly assigned to test functions. + """ + mapping = KeywordMapping.from_item(colitem) + if " " not in keywordexpr: + # special case to allow for simple "-k pass" and "-k 1.3" + return mapping[keywordexpr] + elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]: + return not mapping[keywordexpr[4:]] + for kwd in keywordexpr.split(): + if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list: + raise UsageError( + "Python keyword '{}' not accepted in expressions passed to '-k'".format( + kwd + ) + ) + try: + return eval(keywordexpr, {}, mapping) + except SyntaxError: + raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr)) diff --git a/contrib/python/pytest/py2/_pytest/mark/structures.py b/contrib/python/pytest/py2/_pytest/mark/structures.py index b9b0e31634..aaebe927be 100644 --- a/contrib/python/pytest/py2/_pytest/mark/structures.py +++ b/contrib/python/pytest/py2/_pytest/mark/structures.py @@ -1,76 +1,76 @@ # -*- coding: utf-8 -*- -import inspect -import warnings -from collections import namedtuple -from operator import attrgetter - -import attr +import inspect +import warnings +from collections import namedtuple +from operator import attrgetter + +import attr import six - + from ..compat import ascii_escaped from ..compat import ATTRS_EQ_FIELD -from ..compat import getfslineno -from ..compat import MappingMixin -from ..compat import NOTSET +from ..compat import getfslineno +from ..compat import MappingMixin +from ..compat import NOTSET from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS -from _pytest.outcomes import fail +from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning - -EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" - - -def alias(name, warning=None): - getter = attrgetter(name) - - def warned(self): - warnings.warn(warning, stacklevel=2) - return getter(self) - - return property(getter if warning is None else warned, doc="alias for " + name) - - -def istestfunc(func): - return ( - hasattr(func, "__call__") - and getattr(func, "__name__", "<lambda>") != "<lambda>" - ) - - -def get_empty_parameterset_mark(config, argnames, func): - from ..nodes import Collector - - requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) - if requested_mark in ("", None, "skip"): - mark = MARK_GEN.skip - elif requested_mark == "xfail": - mark = MARK_GEN.xfail(run=False) - elif requested_mark == "fail_at_collect": - f_name = func.__name__ - _, lineno = getfslineno(func) - raise Collector.CollectError( + +EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" + + +def alias(name, warning=None): + getter = attrgetter(name) + + def warned(self): + warnings.warn(warning, stacklevel=2) + return getter(self) + + return property(getter if warning is None else warned, doc="alias for " + name) + + +def istestfunc(func): + return ( + hasattr(func, "__call__") + and getattr(func, "__name__", "<lambda>") != "<lambda>" + ) + + +def get_empty_parameterset_mark(config, argnames, func): + from ..nodes import Collector + + requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) + if requested_mark in ("", None, "skip"): + mark = MARK_GEN.skip + elif requested_mark == "xfail": + mark = MARK_GEN.xfail(run=False) + 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) - fs, lineno = getfslineno(func) - reason = "got empty parameter set %r, function %s at %s:%d" % ( - argnames, - func.__name__, - fs, - lineno, - ) - return mark(reason=reason) - - -class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): - @classmethod + ) + else: + raise LookupError(requested_mark) + fs, lineno = getfslineno(func) + reason = "got empty parameter set %r, function %s at %s:%d" % ( + argnames, + func.__name__, + fs, + lineno, + ) + return mark(reason=reason) + + +class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): + @classmethod def param(cls, *values, **kwargs): marks = kwargs.pop("marks", ()) - if isinstance(marks, MarkDecorator): - marks = (marks,) - else: - assert isinstance(marks, (tuple, list, set)) - + if isinstance(marks, MarkDecorator): + marks = (marks,) + else: + assert isinstance(marks, (tuple, list, set)) + id_ = kwargs.pop("id", None) if id_ is not None: if not isinstance(id_, six.string_types): @@ -78,63 +78,63 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): "Expected id to be a string, got {}: {!r}".format(type(id_), id_) ) id_ = ascii_escaped(id_) - + if kwargs: warnings.warn( PYTEST_PARAM_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=3 ) - return cls(values, marks, id_) - - @classmethod + return cls(values, marks, id_) + + @classmethod def extract_from(cls, parameterset, force_tuple=False): - """ - :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 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 + enforce tuple wrapping so single argument tuple values + don't get decomposed and break tests + """ + + if isinstance(parameterset, cls): + return parameterset if force_tuple: - return cls.param(parameterset) + return cls.param(parameterset) else: return cls(parameterset, marks=[], id=None) - + @staticmethod def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): - 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 def _parse_parametrize_parameters(argvalues, force_tuple): return [ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues - ] + ] @classmethod def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) - del argvalues - - if parameters: - # check all parameter sets have the correct number of values - for param in parameters: - if len(param.values) != len(argnames): + del argvalues + + if parameters: + # check all parameter sets have the correct number of values + 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=function_definition.nodeid, @@ -145,130 +145,130 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): ), pytrace=False, ) - else: - # empty parameter set (likely computed at runtime): create a single + 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 - - -@attr.s(frozen=True) -class Mark(object): - #: name of the mark - name = attr.ib(type=str) - #: positional arguments of the mark decorator + mark = get_empty_parameterset_mark(config, argnames, func) + parameters.append( + ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) + ) + return argnames, parameters + + +@attr.s(frozen=True) +class Mark(object): + #: name of the mark + name = attr.ib(type=str) + #: positional arguments of the mark decorator args = attr.ib() # List[object] - #: keyword arguments of the mark decorator + #: keyword arguments of the mark decorator kwargs = attr.ib() # Dict[str, object] - - def combined_with(self, other): - """ - :param other: the mark to combine with - :type other: Mark - :rtype: Mark - + + def combined_with(self, other): + """ + :param other: the mark to combine with + :type other: Mark + :rtype: Mark + combines by appending args and merging the mappings - """ - assert self.name == other.name - return Mark( - self.name, self.args + other.args, dict(self.kwargs, **other.kwargs) - ) - - -@attr.s -class MarkDecorator(object): - """ A decorator for test functions and test classes. When applied - it will create :class:`MarkInfo` objects which may be - :ref:`retrieved by hooks as item keywords <excontrolskip>`. - MarkDecorator instances are often created like this:: - - 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 - - When a MarkDecorator instance 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 itself to the class so it - gets applied automatically to all test cases found in that class. - 2. If called with a single function as its only positional argument and - no additional keyword arguments, it attaches a MarkInfo object to the - function, containing all the arguments already stored internally in - the MarkDecorator. - 3. When called in any other case, it performs a 'fake construction' call, - i.e. 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 MarkDecorator objects from storing only a - single function or class reference as their positional argument with no - additional keyword or positional arguments. - - """ - - mark = attr.ib(validator=attr.validators.instance_of(Mark)) - - name = alias("mark.name") - args = alias("mark.args") - kwargs = alias("mark.kwargs") - - @property - def markname(self): - return self.name # for backward-compat (2.4.1 had this attr) - - def __eq__(self, other): - return self.mark == other.mark if isinstance(other, MarkDecorator) else False - - def __repr__(self): - return "<MarkDecorator %r>" % (self.mark,) - - def with_args(self, *args, **kwargs): - """ return a MarkDecorator with extra arguments added - - unlike call this can be used even if the sole argument is a callable/class - - :return: MarkDecorator - """ - - mark = Mark(self.name, args, kwargs) - return self.__class__(self.mark.combined_with(mark)) - - def __call__(self, *args, **kwargs): - """ if passed a single callable argument: decorate it with mark info. - otherwise add *args/**kwargs in-place to mark information. """ - if args and not kwargs: - func = args[0] - is_class = inspect.isclass(func) - if len(args) == 1 and (istestfunc(func) or is_class): + """ + assert self.name == other.name + return Mark( + self.name, self.args + other.args, dict(self.kwargs, **other.kwargs) + ) + + +@attr.s +class MarkDecorator(object): + """ A decorator for test functions and test classes. When applied + it will create :class:`MarkInfo` objects which may be + :ref:`retrieved by hooks as item keywords <excontrolskip>`. + MarkDecorator instances are often created like this:: + + 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 + + When a MarkDecorator instance 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 itself to the class so it + gets applied automatically to all test cases found in that class. + 2. If called with a single function as its only positional argument and + no additional keyword arguments, it attaches a MarkInfo object to the + function, containing all the arguments already stored internally in + the MarkDecorator. + 3. When called in any other case, it performs a 'fake construction' call, + i.e. 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 MarkDecorator objects from storing only a + single function or class reference as their positional argument with no + additional keyword or positional arguments. + + """ + + mark = attr.ib(validator=attr.validators.instance_of(Mark)) + + name = alias("mark.name") + args = alias("mark.args") + kwargs = alias("mark.kwargs") + + @property + def markname(self): + return self.name # for backward-compat (2.4.1 had this attr) + + def __eq__(self, other): + return self.mark == other.mark if isinstance(other, MarkDecorator) else False + + def __repr__(self): + return "<MarkDecorator %r>" % (self.mark,) + + def with_args(self, *args, **kwargs): + """ return a MarkDecorator with extra arguments added + + unlike call this can be used even if the sole argument is a callable/class + + :return: MarkDecorator + """ + + mark = Mark(self.name, args, kwargs) + return self.__class__(self.mark.combined_with(mark)) + + def __call__(self, *args, **kwargs): + """ if passed a single callable argument: decorate it with mark info. + otherwise add *args/**kwargs in-place to mark information. """ + 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) - - -def get_unpacked_marks(obj): - """ - 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) - - -def normalize_mark_list(mark_list): - """ - normalizes marker decorating helpers to mark objects - - :type mark_list: List[Union[Mark, Markdecorator]] - :rtype: List[Mark] - """ + return func + return self.with_args(*args, **kwargs) + + +def get_unpacked_marks(obj): + """ + 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) + + +def normalize_mark_list(mark_list): + """ + normalizes marker decorating helpers to mark objects + + :type mark_list: List[Union[Mark, Markdecorator]] + :rtype: List[Mark] + """ extracted = [ getattr(mark, "mark", mark) for mark in mark_list ] # unpack MarkDecorator @@ -276,38 +276,38 @@ def normalize_mark_list(mark_list): if not isinstance(mark, Mark): raise TypeError("got {!r} instead of Mark".format(mark)) return [x for x in extracted if isinstance(x, Mark)] - - -def store_mark(obj, mark): - """store a Mark on an object - this is used to implement the Mark declarations/decorators correctly - """ - 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] - - -class MarkGenerator(object): - """ Factory for :class:`MarkDecorator` objects - exposed as - a ``pytest.mark`` singleton instance. Example:: - - import pytest - @pytest.mark.slowtest - def test_function(): - pass - - will set a 'slowtest' :class:`MarkInfo` object - on the ``test_function`` object. """ - - _config = None + + +def store_mark(obj, mark): + """store a Mark on an object + this is used to implement the Mark declarations/decorators correctly + """ + 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] + + +class MarkGenerator(object): + """ Factory for :class:`MarkDecorator` objects - exposed as + a ``pytest.mark`` singleton instance. Example:: + + import pytest + @pytest.mark.slowtest + def test_function(): + pass + + will set a 'slowtest' :class:`MarkInfo` object + on the ``test_function`` object. """ + + _config = None _markers = set() - - def __getattr__(self, name): - if name[0] == "_": - raise AttributeError("Marker name must NOT start with underscore") - if self._config is not None: + def __getattr__(self, name): + if name[0] == "_": + raise AttributeError("Marker name must NOT start with underscore") + + 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! @@ -338,76 +338,76 @@ class MarkGenerator(object): PytestUnknownMarkWarning, ) - return MarkDecorator(Mark(name, (), {})) - - -MARK_GEN = MarkGenerator() - - -class NodeKeywords(MappingMixin): - def __init__(self, node): - self.node = node - self.parent = node.parent - self._markers = {node.name: True} - - def __getitem__(self, key): - try: - return self._markers[key] - except KeyError: - if self.parent is None: - raise - return self.parent.keywords[key] - - def __setitem__(self, key, value): - self._markers[key] = value - - def __delitem__(self, key): - raise ValueError("cannot delete key in keywords dict") - - def __iter__(self): - seen = self._seen() - return iter(seen) - - def _seen(self): - seen = set(self._markers) - if self.parent is not None: - seen.update(self.parent.keywords) - return seen - - def __len__(self): - return len(self._seen()) - - def __repr__(self): - return "<NodeKeywords for node %s>" % (self.node,) - - + return MarkDecorator(Mark(name, (), {})) + + +MARK_GEN = MarkGenerator() + + +class NodeKeywords(MappingMixin): + def __init__(self, node): + self.node = node + self.parent = node.parent + self._markers = {node.name: True} + + def __getitem__(self, key): + try: + return self._markers[key] + except KeyError: + if self.parent is None: + raise + return self.parent.keywords[key] + + def __setitem__(self, key, value): + self._markers[key] = value + + def __delitem__(self, key): + raise ValueError("cannot delete key in keywords dict") + + def __iter__(self): + seen = self._seen() + return iter(seen) + + def _seen(self): + seen = set(self._markers) + if self.parent is not None: + seen.update(self.parent.keywords) + return seen + + def __len__(self): + return len(self._seen()) + + def __repr__(self): + return "<NodeKeywords for node %s>" % (self.node,) + + # mypy cannot find this overload, remove when on attrs>=19.2 @attr.s(hash=False, **{ATTRS_EQ_FIELD: False}) # type: ignore -class NodeMarkers(object): - """ - internal structure for storing marks belonging to a node - - ..warning:: - - unstable api - - """ - - own_markers = attr.ib(default=attr.Factory(list)) - - def update(self, add_markers): - """update the own markers - """ - self.own_markers.extend(add_markers) - - def find(self, name): - """ - find markers in own nodes or parent nodes - needs a better place - """ - for mark in self.own_markers: - if mark.name == name: - yield mark - - def __iter__(self): - return iter(self.own_markers) +class NodeMarkers(object): + """ + internal structure for storing marks belonging to a node + + ..warning:: + + unstable api + + """ + + own_markers = attr.ib(default=attr.Factory(list)) + + def update(self, add_markers): + """update the own markers + """ + self.own_markers.extend(add_markers) + + def find(self, name): + """ + find markers in own nodes or parent nodes + needs a better place + """ + for mark in self.own_markers: + if mark.name == name: + yield mark + + def __iter__(self): + return iter(self.own_markers) diff --git a/contrib/python/pytest/py2/_pytest/monkeypatch.py b/contrib/python/pytest/py2/_pytest/monkeypatch.py index 7a82faad82..e8671b0c70 100644 --- a/contrib/python/pytest/py2/_pytest/monkeypatch.py +++ b/contrib/python/pytest/py2/_pytest/monkeypatch.py @@ -1,274 +1,274 @@ # -*- coding: utf-8 -*- -""" monkeypatching and mocking functionality. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os -import re -import sys -import warnings -from contextlib import contextmanager - -import six - -import pytest -from _pytest.fixtures import fixture -from _pytest.pathlib import Path - -RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") - - -@fixture -def monkeypatch(): - """The returned ``monkeypatch`` fixture provides these - helper 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) - - 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() - - -def resolve(name): - # 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 - # we use explicit un-nesting of the handling block in order - # to avoid nested exceptions on python 3 - try: - __import__(used) - except ImportError as ex: - # str is used for py2 vs py3 - expected = str(ex).split()[-1] - if expected == used: - raise - else: - raise ImportError("import error in %s: %s" % (used, ex)) - found = annotated_getattr(found, part, used) - return found - - -def annotated_getattr(obj, name, ann): - try: - obj = getattr(obj, name) - except AttributeError: - raise AttributeError( - "%r object at %s has no attribute %r" % (type(obj).__name__, ann, name) - ) - return obj - - -def derive_importpath(import_path, raising): - if not isinstance(import_path, six.string_types) or "." not in import_path: - raise TypeError("must be absolute import path string, not %r" % (import_path,)) - module, attr = import_path.rsplit(".", 1) - target = resolve(module) - if raising: - annotated_getattr(target, attr, ann=module) - return attr, target - - -class Notset(object): - def __repr__(self): - return "<notset>" - - -notset = Notset() - - -class MonkeyPatch(object): - """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. - """ - - def __init__(self): - self._setattr = [] - self._setitem = [] - self._cwd = None - self._savesyspath = None - - @contextmanager - def context(self): - """ - Context manager that returns a new :class:`MonkeyPatch` object which - undoes any patching done inside the ``with`` block upon exit: - - .. code-block:: python - - import functools - 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>`_. - """ - m = MonkeyPatch() - try: - yield m - finally: - m.undo() - - def setattr(self, target, name, value=notset, raising=True): - """ Set attribute value on target, memorizing the old value. - By default raise AttributeError if the attribute did not exist. - - For convenience you can specify a string as ``target`` which - will be interpreted as a dotted import path, with the last part - being the attribute name. Example: - ``monkeypatch.setattr("os.getcwd", lambda: "/")`` - would set the ``getcwd`` function of the ``os`` module. - - The ``raising`` value determines if the setattr should fail - if the attribute is not already present (defaults to True - which means it will raise). - """ - __tracebackhide__ = True - import inspect - - if value is notset: - if not isinstance(target, six.string_types): - 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) - - oldval = getattr(target, name, notset) - if raising and oldval is notset: - raise AttributeError("%r has no attribute %r" % (target, name)) - - # 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, name=notset, raising=True): - """ Delete attribute ``name`` from ``target``, by default raise - AttributeError it the attribute did not previously exist. - - 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 ``raising`` is set to False, no exception will be raised if the - attribute is missing. - """ - __tracebackhide__ = True +""" monkeypatching and mocking functionality. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import re +import sys +import warnings +from contextlib import contextmanager + +import six + +import pytest +from _pytest.fixtures import fixture +from _pytest.pathlib import Path + +RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$") + + +@fixture +def monkeypatch(): + """The returned ``monkeypatch`` fixture provides these + helper 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) + + 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() + + +def resolve(name): + # 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 + # we use explicit un-nesting of the handling block in order + # to avoid nested exceptions on python 3 + try: + __import__(used) + except ImportError as ex: + # str is used for py2 vs py3 + expected = str(ex).split()[-1] + if expected == used: + raise + else: + raise ImportError("import error in %s: %s" % (used, ex)) + found = annotated_getattr(found, part, used) + return found + + +def annotated_getattr(obj, name, ann): + try: + obj = getattr(obj, name) + except AttributeError: + raise AttributeError( + "%r object at %s has no attribute %r" % (type(obj).__name__, ann, name) + ) + return obj + + +def derive_importpath(import_path, raising): + if not isinstance(import_path, six.string_types) or "." not in import_path: + raise TypeError("must be absolute import path string, not %r" % (import_path,)) + module, attr = import_path.rsplit(".", 1) + target = resolve(module) + if raising: + annotated_getattr(target, attr, ann=module) + return attr, target + + +class Notset(object): + def __repr__(self): + return "<notset>" + + +notset = Notset() + + +class MonkeyPatch(object): + """ Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes. + """ + + def __init__(self): + self._setattr = [] + self._setitem = [] + self._cwd = None + self._savesyspath = None + + @contextmanager + def context(self): + """ + Context manager that returns a new :class:`MonkeyPatch` object which + undoes any patching done inside the ``with`` block upon exit: + + .. code-block:: python + + import functools + 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>`_. + """ + m = MonkeyPatch() + try: + yield m + finally: + m.undo() + + def setattr(self, target, name, value=notset, raising=True): + """ Set attribute value on target, memorizing the old value. + By default raise AttributeError if the attribute did not exist. + + For convenience you can specify a string as ``target`` which + will be interpreted as a dotted import path, with the last part + being the attribute name. Example: + ``monkeypatch.setattr("os.getcwd", lambda: "/")`` + would set the ``getcwd`` function of the ``os`` module. + + The ``raising`` value determines if the setattr should fail + if the attribute is not already present (defaults to True + which means it will raise). + """ + __tracebackhide__ = True import inspect - if name is notset: - if not isinstance(target, six.string_types): - 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: + if value is notset: + if not isinstance(target, six.string_types): + 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) + + oldval = getattr(target, name, notset) + if raising and oldval is notset: + raise AttributeError("%r has no attribute %r" % (target, name)) + + # 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, name=notset, raising=True): + """ Delete attribute ``name`` from ``target``, by default raise + AttributeError it the attribute did not previously exist. + + 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 ``raising`` is set to False, no exception will be raised if the + attribute is missing. + """ + __tracebackhide__ = True + import inspect + + if name is notset: + if not isinstance(target, six.string_types): + 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) - - def setitem(self, dic, name, value): - """ Set dictionary entry ``name`` to value. """ - self._setitem.append((dic, name, dic.get(name, notset))) - dic[name] = value - - def delitem(self, dic, name, raising=True): - """ Delete ``name`` from dict. Raise KeyError if it doesn't exist. - - If ``raising`` is set to False, no exception will be raised if the - key is missing. - """ - if name not in dic: - if raising: - raise KeyError(name) - else: - self._setitem.append((dic, name, dic.get(name, notset))) - del dic[name] - - def _warn_if_env_name_is_not_str(self, name): - """On Python 2, warn if the given environment variable name is not a native str (#4056)""" - if six.PY2 and not isinstance(name, str): - warnings.warn( - pytest.PytestWarning( - "Environment variable name {!r} should be str".format(name) - ) - ) - - def setenv(self, name, value, prepend=None): - """ Set environment variable ``name`` to ``value``. If ``prepend`` - is a character, read the current environment variable value - and prepend the ``value`` adjoined with the ``prepend`` character.""" - if not isinstance(value, str): - warnings.warn( - pytest.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._warn_if_env_name_is_not_str(name) - self.setitem(os.environ, name, value) - - def delenv(self, name, raising=True): - """ Delete ``name`` from the environment. Raise KeyError if it does - not exist. - - If ``raising`` is set to False, no exception will be raised if the - environment variable is missing. - """ - self._warn_if_env_name_is_not_str(name) - self.delitem(os.environ, name, raising=raising) - - def syspath_prepend(self, path): - """ Prepend ``path`` to ``sys.path`` list of import locations. """ + delattr(target, name) + + def setitem(self, dic, name, value): + """ Set dictionary entry ``name`` to value. """ + self._setitem.append((dic, name, dic.get(name, notset))) + dic[name] = value + + def delitem(self, dic, name, raising=True): + """ Delete ``name`` from dict. Raise KeyError if it doesn't exist. + + If ``raising`` is set to False, no exception will be raised if the + key is missing. + """ + if name not in dic: + if raising: + raise KeyError(name) + else: + self._setitem.append((dic, name, dic.get(name, notset))) + del dic[name] + + def _warn_if_env_name_is_not_str(self, name): + """On Python 2, warn if the given environment variable name is not a native str (#4056)""" + if six.PY2 and not isinstance(name, str): + warnings.warn( + pytest.PytestWarning( + "Environment variable name {!r} should be str".format(name) + ) + ) + + def setenv(self, name, value, prepend=None): + """ Set environment variable ``name`` to ``value``. If ``prepend`` + is a character, read the current environment variable value + and prepend the ``value`` adjoined with the ``prepend`` character.""" + if not isinstance(value, str): + warnings.warn( + pytest.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._warn_if_env_name_is_not_str(name) + self.setitem(os.environ, name, value) + + def delenv(self, name, raising=True): + """ Delete ``name`` from the environment. Raise KeyError if it does + not exist. + + If ``raising`` is set to False, no exception will be raised if the + environment variable is missing. + """ + self._warn_if_env_name_is_not_str(name) + self.delitem(os.environ, name, raising=raising) + + def syspath_prepend(self, path): + """ 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)) @@ -284,53 +284,53 @@ class MonkeyPatch(object): invalidate_caches() - def chdir(self, path): - """ 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): - # modern python uses the fspath protocol here LEGACY - os.chdir(str(path)) - else: - os.chdir(path) - - def undo(self): - """ 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[:] = [] - for dictionary, name, value in reversed(self._setitem): - if value is notset: - try: - del dictionary[name] - except KeyError: - pass # was already deleted, so we have the desired state - else: - dictionary[name] = 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 + def chdir(self, path): + """ 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): + # modern python uses the fspath protocol here LEGACY + os.chdir(str(path)) + else: + os.chdir(path) + + def undo(self): + """ 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[:] = [] + for dictionary, name, value in reversed(self._setitem): + if value is notset: + try: + del dictionary[name] + except KeyError: + pass # was already deleted, so we have the desired state + else: + dictionary[name] = 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 diff --git a/contrib/python/pytest/py2/_pytest/nodes.py b/contrib/python/pytest/py2/_pytest/nodes.py index 271c92dbc2..206e9ae163 100644 --- a/contrib/python/pytest/py2/_pytest/nodes.py +++ b/contrib/python/pytest/py2/_pytest/nodes.py @@ -1,332 +1,332 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os -import warnings - -import py -import six - -import _pytest._code -from _pytest.compat import getfslineno -from _pytest.mark.structures import NodeKeywords -from _pytest.outcomes import fail - -SEP = "/" - -tracebackcutdir = py.path.local(_pytest.__file__).dirpath() - - -def _splitnode(nodeid): - """Split a nodeid into constituent 'parts'. - - Node IDs are strings, and can be things like: - '' - 'testing/code' - 'testing/code/test_excinfo.py' - 'testing/code/test_excinfo.py::TestFormattedExcinfo' - - Return values are lists e.g. - [] - ['testing', 'code'] - ['testing', 'code', 'test_excinfo.py'] - ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()'] - """ - if nodeid == "": - # If there is no root node at all, return an empty list so the caller's logic can remain sane - return [] - parts = nodeid.split(SEP) - # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar' - parts[-1:] = parts[-1].split("::") - return parts - - -def ischildnode(baseid, nodeid): - """Return True if the nodeid is a child node of the baseid. - - E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp' - """ - base_parts = _splitnode(baseid) - node_parts = _splitnode(nodeid) - if len(node_parts) < len(base_parts): - return False - return node_parts[: len(base_parts)] == base_parts - - -class Node(object): - """ base class for Collector and Item the test collection tree. - Collector subclasses have children, Items are terminal nodes.""" - - def __init__( - self, name, parent=None, config=None, session=None, fspath=None, nodeid=None - ): - #: a unique name within the scope of the parent node - self.name = name - - #: the parent collector node. - self.parent = parent - - #: the pytest config object - self.config = config or parent.config - - #: the session this node is part of - self.session = session or parent.session - - #: filesystem path where this node was collected from (can be None) - self.fspath = fspath or getattr(parent, "fspath", None) - - #: keywords/markers collected from all scopes - self.keywords = NodeKeywords(self) - - #: the marker objects belonging to this node - self.own_markers = [] - - #: allow adding of extra keywords to use for matching - self.extra_keyword_matches = set() - - # used for storing artificial fixturedefs for direct parametrization - self._name2pseudofixturedef = {} - - if nodeid is not None: - assert "::()" not in nodeid - self._nodeid = nodeid - else: - self._nodeid = self.parent.nodeid - if self.name != "()": - self._nodeid += "::" + self.name - - @property - def ihook(self): - """ fspath sensitive hook proxy used to call pytest hooks""" - return self.session.gethookproxy(self.fspath) - - def __repr__(self): +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import warnings + +import py +import six + +import _pytest._code +from _pytest.compat import getfslineno +from _pytest.mark.structures import NodeKeywords +from _pytest.outcomes import fail + +SEP = "/" + +tracebackcutdir = py.path.local(_pytest.__file__).dirpath() + + +def _splitnode(nodeid): + """Split a nodeid into constituent 'parts'. + + Node IDs are strings, and can be things like: + '' + 'testing/code' + 'testing/code/test_excinfo.py' + 'testing/code/test_excinfo.py::TestFormattedExcinfo' + + Return values are lists e.g. + [] + ['testing', 'code'] + ['testing', 'code', 'test_excinfo.py'] + ['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()'] + """ + if nodeid == "": + # If there is no root node at all, return an empty list so the caller's logic can remain sane + return [] + parts = nodeid.split(SEP) + # Replace single last element 'test_foo.py::Bar' with multiple elements 'test_foo.py', 'Bar' + parts[-1:] = parts[-1].split("::") + return parts + + +def ischildnode(baseid, nodeid): + """Return True if the nodeid is a child node of the baseid. + + E.g. 'foo/bar::Baz' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp' + """ + base_parts = _splitnode(baseid) + node_parts = _splitnode(nodeid) + if len(node_parts) < len(base_parts): + return False + return node_parts[: len(base_parts)] == base_parts + + +class Node(object): + """ base class for Collector and Item the test collection tree. + Collector subclasses have children, Items are terminal nodes.""" + + def __init__( + self, name, parent=None, config=None, session=None, fspath=None, nodeid=None + ): + #: a unique name within the scope of the parent node + self.name = name + + #: the parent collector node. + self.parent = parent + + #: the pytest config object + self.config = config or parent.config + + #: the session this node is part of + self.session = session or parent.session + + #: filesystem path where this node was collected from (can be None) + self.fspath = fspath or getattr(parent, "fspath", None) + + #: keywords/markers collected from all scopes + self.keywords = NodeKeywords(self) + + #: the marker objects belonging to this node + self.own_markers = [] + + #: allow adding of extra keywords to use for matching + self.extra_keyword_matches = set() + + # used for storing artificial fixturedefs for direct parametrization + self._name2pseudofixturedef = {} + + if nodeid is not None: + assert "::()" not in nodeid + self._nodeid = nodeid + else: + self._nodeid = self.parent.nodeid + if self.name != "()": + self._nodeid += "::" + self.name + + @property + def ihook(self): + """ fspath sensitive hook proxy used to call pytest hooks""" + return self.session.gethookproxy(self.fspath) + + def __repr__(self): return "<%s %s>" % (self.__class__.__name__, getattr(self, "name", None)) - + def warn(self, warning): - """Issue a warning for this item. - + """Issue a warning for this item. + Warnings will be displayed after the test session, unless explicitly suppressed - + :param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning. - + :raise ValueError: if ``warning`` instance is not a subclass of PytestWarning. - + Example usage: - - .. code-block:: python - - node.warn(PytestWarning("some message")) - - """ - from _pytest.warning_types import PytestWarning - - if not isinstance(warning, PytestWarning): - raise ValueError( - "warning must be an instance of PytestWarning or subclass, got {!r}".format( - warning - ) - ) - path, lineno = get_fslocation_from_item(self) - warnings.warn_explicit( - warning, - category=None, - filename=str(path), - lineno=lineno + 1 if lineno is not None else None, - ) - - # methods for ordering nodes - @property - def nodeid(self): - """ a ::-separated string denoting its collection tree address. """ - return self._nodeid - - def __hash__(self): - return hash(self.nodeid) - - def setup(self): - pass - - def teardown(self): - pass - - def listchain(self): - """ return list of all parent collectors up to self, - starting from root of collection tree. """ - chain = [] - item = self - while item is not None: - chain.append(item) - item = item.parent - chain.reverse() - return chain - - def add_marker(self, marker, append=True): - """dynamically add a marker object to the node. - - :type marker: ``str`` or ``pytest.mark.*`` object - :param marker: - ``append=True`` whether to append the marker, - if ``False`` insert at position ``0``. - """ - from _pytest.mark import MarkDecorator, MARK_GEN - - if isinstance(marker, six.string_types): - marker = getattr(MARK_GEN, marker) - elif not isinstance(marker, MarkDecorator): - raise ValueError("is not a string or pytest.mark.* Marker") - self.keywords[marker.name] = marker - if append: - self.own_markers.append(marker.mark) - else: - self.own_markers.insert(0, marker.mark) - - def iter_markers(self, name=None): - """ - :param name: if given, filter the results by the name attribute - - iterate over all markers of the node - """ - return (x[1] for x in self.iter_markers_with_node(name=name)) - - def iter_markers_with_node(self, name=None): - """ - :param name: if given, filter the results by the name attribute - - iterate over all markers of the node - returns sequence of tuples (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 - - def get_closest_marker(self, name, default=None): - """return the first marker matching the name, from closest (for example function) to farther level (for example - module level). - - :param default: fallback return value of no marker was found - :param name: name to filter by - """ - return next(self.iter_markers(name=name), default) - - def listextrakeywords(self): - """ Return a set of all extra keywords in self and any parents.""" - extra_keywords = set() - for item in self.listchain(): - extra_keywords.update(item.extra_keyword_matches) - return extra_keywords - - def listnames(self): - return [x.name for x in self.listchain()] - - def addfinalizer(self, fin): - """ 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) - - def getparent(self, cls): - """ get the next parent node (including ourself) - which is an instance of the given class""" - current = self - while current and not isinstance(current, cls): - current = current.parent - return current - - def _prunetraceback(self, excinfo): - pass - - def _repr_failure_py(self, excinfo, style=None): - if excinfo.errisinstance(fail.Exception): - if not excinfo.value.pytrace: - return six.text_type(excinfo.value) - fm = self.session._fixturemanager - if excinfo.errisinstance(fm.FixtureLookupError): - return excinfo.value.formatrepr() - tbfilter = True + + .. code-block:: python + + node.warn(PytestWarning("some message")) + + """ + from _pytest.warning_types import PytestWarning + + if not isinstance(warning, PytestWarning): + raise ValueError( + "warning must be an instance of PytestWarning or subclass, got {!r}".format( + warning + ) + ) + path, lineno = get_fslocation_from_item(self) + warnings.warn_explicit( + warning, + category=None, + filename=str(path), + lineno=lineno + 1 if lineno is not None else None, + ) + + # methods for ordering nodes + @property + def nodeid(self): + """ a ::-separated string denoting its collection tree address. """ + return self._nodeid + + def __hash__(self): + return hash(self.nodeid) + + def setup(self): + pass + + def teardown(self): + pass + + def listchain(self): + """ return list of all parent collectors up to self, + starting from root of collection tree. """ + chain = [] + item = self + while item is not None: + chain.append(item) + item = item.parent + chain.reverse() + return chain + + def add_marker(self, marker, append=True): + """dynamically add a marker object to the node. + + :type marker: ``str`` or ``pytest.mark.*`` object + :param marker: + ``append=True`` whether to append the marker, + if ``False`` insert at position ``0``. + """ + from _pytest.mark import MarkDecorator, MARK_GEN + + if isinstance(marker, six.string_types): + marker = getattr(MARK_GEN, marker) + elif not isinstance(marker, MarkDecorator): + raise ValueError("is not a string or pytest.mark.* Marker") + self.keywords[marker.name] = marker + if append: + self.own_markers.append(marker.mark) + else: + self.own_markers.insert(0, marker.mark) + + def iter_markers(self, name=None): + """ + :param name: if given, filter the results by the name attribute + + iterate over all markers of the node + """ + return (x[1] for x in self.iter_markers_with_node(name=name)) + + def iter_markers_with_node(self, name=None): + """ + :param name: if given, filter the results by the name attribute + + iterate over all markers of the node + returns sequence of tuples (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 + + def get_closest_marker(self, name, default=None): + """return the first marker matching the name, from closest (for example function) to farther level (for example + module level). + + :param default: fallback return value of no marker was found + :param name: name to filter by + """ + return next(self.iter_markers(name=name), default) + + def listextrakeywords(self): + """ Return a set of all extra keywords in self and any parents.""" + extra_keywords = set() + for item in self.listchain(): + extra_keywords.update(item.extra_keyword_matches) + return extra_keywords + + def listnames(self): + return [x.name for x in self.listchain()] + + def addfinalizer(self, fin): + """ 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) + + def getparent(self, cls): + """ get the next parent node (including ourself) + which is an instance of the given class""" + current = self + while current and not isinstance(current, cls): + current = current.parent + return current + + def _prunetraceback(self, excinfo): + pass + + def _repr_failure_py(self, excinfo, style=None): + if excinfo.errisinstance(fail.Exception): + if not excinfo.value.pytrace: + return six.text_type(excinfo.value) + fm = self.session._fixturemanager + if excinfo.errisinstance(fm.FixtureLookupError): + return excinfo.value.formatrepr() + tbfilter = True 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 - tbfilter = False # prunetraceback already does it - 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 + tbfilter = False # prunetraceback already does it + 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 - - try: - os.getcwd() - abspath = False - except OSError: - abspath = True - - return excinfo.getrepr( - funcargs=True, - abspath=abspath, + truncate_locals = False + else: + truncate_locals = True + + try: + os.getcwd() + abspath = False + except OSError: + abspath = True + + return excinfo.getrepr( + funcargs=True, + abspath=abspath, showlocals=self.config.getoption("showlocals", False), - style=style, - tbfilter=tbfilter, - truncate_locals=truncate_locals, - ) - - repr_failure = _repr_failure_py - - -def get_fslocation_from_item(item): - """Tries to extract the actual location from an item, depending on available attributes: - - * "fslocation": a pair (path, lineno) - * "obj": a Python object that the item wraps. - * "fspath": just a path - - :rtype: a tuple of (str|LocalPath, int) with filename and line number. - """ - result = getattr(item, "location", None) - if result is not None: - return result[:2] - obj = getattr(item, "obj", None) - if obj is not None: - return getfslineno(obj) - return getattr(item, "fspath", "unknown location"), -1 - - -class Collector(Node): - """ Collector instances create children through collect() - and thus iteratively build a tree. - """ - - class CollectError(Exception): - """ an error during collection, contains a custom message. """ - - def collect(self): - """ returns a list of children (items and collectors) - for this collection node. - """ - raise NotImplementedError("abstract") - - def repr_failure(self, excinfo): - """ represent a collection failure. """ - if excinfo.errisinstance(self.CollectError): - exc = excinfo.value - return str(exc.args[0]) - + style=style, + tbfilter=tbfilter, + truncate_locals=truncate_locals, + ) + + repr_failure = _repr_failure_py + + +def get_fslocation_from_item(item): + """Tries to extract the actual location from an item, depending on available attributes: + + * "fslocation": a pair (path, lineno) + * "obj": a Python object that the item wraps. + * "fspath": just a path + + :rtype: a tuple of (str|LocalPath, int) with filename and line number. + """ + result = getattr(item, "location", None) + if result is not None: + return result[:2] + obj = getattr(item, "obj", None) + if obj is not None: + return getfslineno(obj) + return getattr(item, "fspath", "unknown location"), -1 + + +class Collector(Node): + """ Collector instances create children through collect() + and thus iteratively build a tree. + """ + + class CollectError(Exception): + """ an error during collection, contains a custom message. """ + + def collect(self): + """ returns a list of children (items and collectors) + for this collection node. + """ + raise NotImplementedError("abstract") + + def repr_failure(self, excinfo): + """ represent a collection failure. """ + if excinfo.errisinstance(self.CollectError): + exc = excinfo.value + return str(exc.args[0]) + # Respect explicit tbstyle option, but default to "short" # (None._repr_failure_py defaults to "long" without "fulltrace" option). tbstyle = self.config.getoption("tbstyle", "auto") @@ -335,95 +335,95 @@ class Collector(Node): return self._repr_failure_py(excinfo, style=tbstyle) - def _prunetraceback(self, excinfo): - 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, parent=None, config=None, session=None, nodeid=None): - fspath = py.path.local(fspath) # xxx only for test_resultlog.py? - 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(FSCollector, self).__init__( - name, parent, config, session, nodeid=nodeid, fspath=fspath - ) - - -class File(FSCollector): - """ base class for collecting tests from a file. """ - - -class Item(Node): - """ a basic test invocation item. Note that for a single function - there might be multiple test invocation items. - """ - - nextitem = None - - def __init__(self, name, parent=None, config=None, session=None, nodeid=None): - super(Item, self).__init__(name, parent, config, session, nodeid=nodeid) - self._report_sections = [] - - #: user properties is a list of tuples (name, value) that holds user - #: defined properties for this test. - self.user_properties = [] - - def add_report_section(self, when, key, content): - """ - Adds 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)) - - def reportinfo(self): - return self.fspath, None, "" - - @property - def location(self): - try: - return self._location - except AttributeError: - location = self.reportinfo() - fspath = self.session._node_location_to_relpath(location[0]) - location = (fspath, location[1], str(location[2])) - self._location = location - return location + def _prunetraceback(self, excinfo): + 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, parent=None, config=None, session=None, nodeid=None): + fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + 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(FSCollector, self).__init__( + name, parent, config, session, nodeid=nodeid, fspath=fspath + ) + + +class File(FSCollector): + """ base class for collecting tests from a file. """ + + +class Item(Node): + """ a basic test invocation item. Note that for a single function + there might be multiple test invocation items. + """ + + nextitem = None + + def __init__(self, name, parent=None, config=None, session=None, nodeid=None): + super(Item, self).__init__(name, parent, config, session, nodeid=nodeid) + self._report_sections = [] + + #: user properties is a list of tuples (name, value) that holds user + #: defined properties for this test. + self.user_properties = [] + + def add_report_section(self, when, key, content): + """ + Adds 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)) + + def reportinfo(self): + return self.fspath, None, "" + + @property + def location(self): + try: + return self._location + except AttributeError: + location = self.reportinfo() + fspath = self.session._node_location_to_relpath(location[0]) + location = (fspath, location[1], str(location[2])) + self._location = location + return location diff --git a/contrib/python/pytest/py2/_pytest/nose.py b/contrib/python/pytest/py2/_pytest/nose.py index 4605e5a3f7..fbab91da24 100644 --- a/contrib/python/pytest/py2/_pytest/nose.py +++ b/contrib/python/pytest/py2/_pytest/nose.py @@ -1,70 +1,70 @@ # -*- coding: utf-8 -*- -""" run test suites written for nose. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import sys - +""" run test suites written for nose. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys + import six import pytest -from _pytest import python -from _pytest import runner -from _pytest import unittest -from _pytest.config import hookimpl - - -def get_skip_exceptions(): - skip_classes = set() - for module_name in ("unittest", "unittest2", "nose"): - mod = sys.modules.get(module_name) - if hasattr(mod, "SkipTest"): - skip_classes.add(mod.SkipTest) - return tuple(skip_classes) - - -def pytest_runtest_makereport(item, call): - if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): - # let's substitute the excinfo with a pytest.skip one +from _pytest import python +from _pytest import runner +from _pytest import unittest +from _pytest.config import hookimpl + + +def get_skip_exceptions(): + skip_classes = set() + for module_name in ("unittest", "unittest2", "nose"): + mod = sys.modules.get(module_name) + if hasattr(mod, "SkipTest"): + skip_classes.add(mod.SkipTest) + return tuple(skip_classes) + + +def pytest_runtest_makereport(item, call): + if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): + # let's substitute the excinfo with a pytest.skip one call2 = runner.CallInfo.from_call( lambda: pytest.skip(six.text_type(call.excinfo.value)), call.when ) - call.excinfo = call2.excinfo - - -@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") - # 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") - # if hasattr(item.parent, '_nosegensetup'): - # #call_optional(item._nosegensetup, 'teardown') - # del item.parent._nosegensetup - - -def is_potential_nosetest(item): - # 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 - # silently ignoring them - method() - return True + call.excinfo = call2.excinfo + + +@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") + # 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") + # if hasattr(item.parent, '_nosegensetup'): + # #call_optional(item._nosegensetup, 'teardown') + # del item.parent._nosegensetup + + +def is_potential_nosetest(item): + # 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 + # silently ignoring them + method() + return True diff --git a/contrib/python/pytest/py2/_pytest/outcomes.py b/contrib/python/pytest/py2/_pytest/outcomes.py index 2d0babc7f3..4620f957c7 100644 --- a/contrib/python/pytest/py2/_pytest/outcomes.py +++ b/contrib/python/pytest/py2/_pytest/outcomes.py @@ -1,148 +1,148 @@ # -*- coding: utf-8 -*- -""" -exception classes and constants handling test outcomes -as well as functions creating them -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import sys - +""" +exception classes and constants handling test outcomes +as well as functions creating them +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys + from packaging.version import Version - - -class OutcomeException(BaseException): - """ OutcomeException and its subclass instances indicate and - contain info about test and collection outcomes. - """ - - def __init__(self, msg=None, pytrace=True): - BaseException.__init__(self, msg) - self.msg = msg - self.pytrace = pytrace - - def __repr__(self): - if self.msg: - val = self.msg - if isinstance(val, bytes): - val = val.decode("UTF-8", errors="replace") - return val - return "<%s instance>" % (self.__class__.__name__,) - - __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=None, pytrace=True, allow_module_level=False): - 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" - - + + +class OutcomeException(BaseException): + """ OutcomeException and its subclass instances indicate and + contain info about test and collection outcomes. + """ + + def __init__(self, msg=None, pytrace=True): + BaseException.__init__(self, msg) + self.msg = msg + self.pytrace = pytrace + + def __repr__(self): + if self.msg: + val = self.msg + if isinstance(val, bytes): + val = val.decode("UTF-8", errors="replace") + return val + return "<%s instance>" % (self.__class__.__name__,) + + __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=None, pytrace=True, allow_module_level=False): + 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" + + class Exit(Exception): - """ raised for immediate program exits (no tracebacks/summaries)""" - - def __init__(self, msg="unknown reason", returncode=None): - self.msg = msg - self.returncode = returncode + """ raised for immediate program exits (no tracebacks/summaries)""" + + def __init__(self, msg="unknown reason", returncode=None): + self.msg = msg + self.returncode = returncode super(Exit, self).__init__(msg) - - -# exposed helper methods - - -def exit(msg, returncode=None): - """ + + +# exposed helper methods + + +def exit(msg, returncode=None): + """ 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) - - -exit.Exception = Exit - - -def skip(msg="", **kwargs): - """ - Skip an executing test with the given message. - - This function should be called only during testing (setup, call or teardown) or + + :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) + + +exit.Exception = Exit + + +def skip(msg="", **kwargs): + """ + Skip an executing test with the given message. + + 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. - - :kwarg bool allow_module_level: allows this function to be called at - module level, skipping the rest of the module. Default to False. - - .. 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. + + :kwarg bool allow_module_level: allows this function to be called at + module level, skipping the rest of the module. Default to False. + + .. 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 - allow_module_level = kwargs.pop("allow_module_level", False) - if kwargs: + """ + __tracebackhide__ = True + allow_module_level = kwargs.pop("allow_module_level", False) + if kwargs: raise TypeError("unexpected keyword arguments: {}".format(sorted(kwargs))) - raise Skipped(msg=msg, allow_module_level=allow_module_level) - - -skip.Exception = Skipped - - -def fail(msg="", pytrace=True): - """ - 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 the msg represents the full failure information and no - python traceback will be reported. - """ - __tracebackhide__ = True - raise Failed(msg=msg, pytrace=pytrace) - - -fail.Exception = Failed - - -class XFailed(fail.Exception): - """ raised from an explicit call to pytest.xfail() """ - - -def xfail(reason=""): - """ - Imperatively xfail an executing test or setup functions with the given reason. - - This function should be called only during testing (setup, call or teardown). - - .. 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) - - -xfail.Exception = XFailed - - + raise Skipped(msg=msg, allow_module_level=allow_module_level) + + +skip.Exception = Skipped + + +def fail(msg="", pytrace=True): + """ + 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 the msg represents the full failure information and no + python traceback will be reported. + """ + __tracebackhide__ = True + raise Failed(msg=msg, pytrace=pytrace) + + +fail.Exception = Failed + + +class XFailed(fail.Exception): + """ raised from an explicit call to pytest.xfail() """ + + +def xfail(reason=""): + """ + Imperatively xfail an executing test or setup functions with the given reason. + + This function should be called only during testing (setup, call or teardown). + + .. 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) + + +xfail.Exception = XFailed + + def importorskip(modname, minversion=None, reason=None): """Imports and returns the requested module ``modname``, or skip the current test if the module cannot be imported. @@ -152,36 +152,36 @@ def importorskip(modname, minversion=None, reason=None): at least this minimal version, otherwise the test is still skipped. :param str reason: if given, this reason is shown as the message when the module cannot be imported. - """ - import warnings - - __tracebackhide__ = True - compile(modname, "", "eval") # to catch syntaxerrors + """ + import warnings + + __tracebackhide__ = True + compile(modname, "", "eval") # to catch syntaxerrors import_exc = None - - with warnings.catch_warnings(): - # make sure to ignore ImportWarnings that might happen because - # of existing directories with the same name we're trying to - # import but without a __init__.py file - warnings.simplefilter("ignore") - try: - __import__(modname) + + with warnings.catch_warnings(): + # make sure to ignore ImportWarnings that might happen because + # of existing directories with the same name we're trying to + # import but without a __init__.py file + warnings.simplefilter("ignore") + try: + __import__(modname) except ImportError as exc: - # Do not raise chained exception here(#1485) + # Do not raise chained exception here(#1485) import_exc = exc if import_exc: if reason is None: reason = "could not import %r: %s" % (modname, import_exc) raise Skipped(reason, allow_module_level=True) - 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: 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/py2/_pytest/pastebin.py b/contrib/python/pytest/py2/_pytest/pastebin.py index 1150f59105..7a3e80231c 100644 --- a/contrib/python/pytest/py2/_pytest/pastebin.py +++ b/contrib/python/pytest/py2/_pytest/pastebin.py @@ -1,110 +1,110 @@ # -*- coding: utf-8 -*- -""" submit failure or test session information to a pastebin service. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import sys -import tempfile - -import six - -import pytest - - -def pytest_addoption(parser): - 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): - 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 slave node - # when using pytest-xdist, for example - if tr is not None: - # pastebin file will be utf-8 encoded binary file - config._pastebinfile = tempfile.TemporaryFile("w+b") - oldwrite = tr._tw.write - - def tee_write(s, **kwargs): - oldwrite(s, **kwargs) - if isinstance(s, six.text_type): - s = s.encode("utf-8") - config._pastebinfile.write(s) - - tr._tw.write = tee_write - - -def pytest_unconfigure(config): - if hasattr(config, "_pastebinfile"): - # get terminal contents and delete file - config._pastebinfile.seek(0) - sessionlog = config._pastebinfile.read() - config._pastebinfile.close() - del config._pastebinfile - # undo our patching in the terminal reporter - 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) - - -def create_new_paste(contents): - """ - Creates a new paste using bpaste.net service. - - :contents: paste contents as utf-8 encoded bytes - :returns: url to the pasted contents - """ - import re - - if sys.version_info < (3, 0): - from urllib import urlopen, urlencode - else: - from urllib.request import urlopen - from urllib.parse import urlencode - +""" submit failure or test session information to a pastebin service. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys +import tempfile + +import six + +import pytest + + +def pytest_addoption(parser): + 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): + 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 slave node + # when using pytest-xdist, for example + if tr is not None: + # pastebin file will be utf-8 encoded binary file + config._pastebinfile = tempfile.TemporaryFile("w+b") + oldwrite = tr._tw.write + + def tee_write(s, **kwargs): + oldwrite(s, **kwargs) + if isinstance(s, six.text_type): + s = s.encode("utf-8") + config._pastebinfile.write(s) + + tr._tw.write = tee_write + + +def pytest_unconfigure(config): + if hasattr(config, "_pastebinfile"): + # get terminal contents and delete file + config._pastebinfile.seek(0) + sessionlog = config._pastebinfile.read() + config._pastebinfile.close() + del config._pastebinfile + # undo our patching in the terminal reporter + 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) + + +def create_new_paste(contents): + """ + Creates a new paste using bpaste.net service. + + :contents: paste contents as utf-8 encoded bytes + :returns: url to the pasted contents + """ + import re + + if sys.version_info < (3, 0): + from urllib import urlopen, urlencode + else: + from urllib.request import urlopen + from urllib.parse import urlencode + params = {"code": contents, "lexer": "text", "expiry": "1week"} - url = "https://bpaste.net" - response = urlopen(url, data=urlencode(params).encode("ascii")).read() - m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8")) - if m: - return "%s/show/%s" % (url, m.group(1)) - else: - return "bad response: " + response - - -def pytest_terminal_summary(terminalreporter): - import _pytest.config - - if terminalreporter.config.option.pastebin != "failed": - return - tr = terminalreporter - if "failed" in tr.stats: - terminalreporter.write_sep("=", "Sending information to Paste Service") - for rep in terminalreporter.stats.get("failed"): - try: - msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc - except AttributeError: - msg = tr._getfailureheadline(rep) - tw = _pytest.config.create_terminal_writer( - terminalreporter.config, stringio=True - ) - rep.toterminal(tw) - s = tw.stringio.getvalue() - assert len(s) - pastebinurl = create_new_paste(s) - tr.write_line("%s --> %s" % (msg, pastebinurl)) + url = "https://bpaste.net" + response = urlopen(url, data=urlencode(params).encode("ascii")).read() + m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8")) + if m: + return "%s/show/%s" % (url, m.group(1)) + else: + return "bad response: " + response + + +def pytest_terminal_summary(terminalreporter): + import _pytest.config + + if terminalreporter.config.option.pastebin != "failed": + return + tr = terminalreporter + if "failed" in tr.stats: + terminalreporter.write_sep("=", "Sending information to Paste Service") + for rep in terminalreporter.stats.get("failed"): + try: + msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc + except AttributeError: + msg = tr._getfailureheadline(rep) + tw = _pytest.config.create_terminal_writer( + terminalreporter.config, stringio=True + ) + rep.toterminal(tw) + s = tw.stringio.getvalue() + assert len(s) + pastebinurl = create_new_paste(s) + tr.write_line("%s --> %s" % (msg, pastebinurl)) diff --git a/contrib/python/pytest/py2/_pytest/pathlib.py b/contrib/python/pytest/py2/_pytest/pathlib.py index 668bd2b2be..42071f4310 100644 --- a/contrib/python/pytest/py2/_pytest/pathlib.py +++ b/contrib/python/pytest/py2/_pytest/pathlib.py @@ -1,60 +1,60 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import atexit -import errno -import fnmatch -import itertools -import operator -import os -import shutil -import sys -import uuid +import atexit +import errno +import fnmatch +import itertools +import operator +import os +import shutil +import sys +import uuid import warnings from functools import partial -from functools import reduce -from os.path import expanduser -from os.path import expandvars -from os.path import isabs -from os.path import sep -from posixpath import sep as posix_sep - -import six -from six.moves import map - -from .compat import PY36 +from functools import reduce +from os.path import expanduser +from os.path import expandvars +from os.path import isabs +from os.path import sep +from posixpath import sep as posix_sep + +import six +from six.moves import map + +from .compat import PY36 from _pytest.warning_types import PytestWarning - -if PY36: - from pathlib import Path, PurePath -else: - from pathlib2 import Path, PurePath - -__all__ = ["Path", "PurePath"] - - -LOCK_TIMEOUT = 60 * 60 * 3 - -get_lock_path = operator.methodcaller("joinpath", ".lock") - - -def ensure_reset_dir(path): - """ - ensures the given path is an empty directory - """ - if path.exists(): + +if PY36: + from pathlib import Path, PurePath +else: + from pathlib2 import Path, PurePath + +__all__ = ["Path", "PurePath"] + + +LOCK_TIMEOUT = 60 * 60 * 3 + +get_lock_path = operator.methodcaller("joinpath", ".lock") + + +def ensure_reset_dir(path): + """ + ensures the given path is an empty directory + """ + if path.exists(): rm_rf(path) - path.mkdir() - - + path.mkdir() + + def on_rm_rf_error(func, path, exc, **kwargs): """Handles known read-only errors during rmtree. - + The returned value is used only by our own tests. """ start_path = kwargs["start_path"] 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, OSError) and excvalue.errno == errno.ENOENT: @@ -111,270 +111,270 @@ def rm_rf(path): shutil.rmtree(str(path), onerror=onerror) -def find_prefixed(root, prefix): - """finds 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 - - -def extract_suffixes(iter, prefix): - """ - :param iter: iterator over path names - :param prefix: expected prefix of the path names - :returns: the parts of the paths following the prefix - """ - p_len = len(prefix) - for p in iter: - yield p.name[p_len:] - - -def find_suffixes(root, prefix): - """combines find_prefixes and extract_suffixes - """ - return extract_suffixes(find_prefixed(root, prefix), prefix) - - -def parse_num(maybe_num): - """parses number path suffixes, returns -1 on error""" - try: - return int(maybe_num) - except ValueError: - return -1 - - -if six.PY2: - - def _max(iterable, default): - """needed due to python2.7 lacking the default argument for max""" - return reduce(max, iterable, default) - - -else: - _max = max - - -def _force_symlink(root, target, link_to): - """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 testrun - - the presumption being thatin 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 - - -def make_numbered_dir(root, prefix): - """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 - max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1) - new_number = max_existing + 1 - new_path = root.joinpath("{}{}".format(prefix, new_number)) - try: - new_path.mkdir() - except Exception: - pass - else: - _force_symlink(root, prefix + "current", new_path) - return new_path - else: - raise EnvironmentError( - "could not create numbered dir with prefix " - "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) - ) - - -def create_cleanup_lock(p): - """crates 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) - except OSError as e: - if e.errno == errno.EEXIST: - six.raise_from( - EnvironmentError("cannot create lockfile in {path}".format(path=p)), e - ) - else: - raise - else: - pid = os.getpid() - spid = str(pid) - if not isinstance(spid, bytes): - spid = spid.encode("ascii") - os.write(fd, spid) - os.close(fd) - if not lock_path.is_file(): - raise EnvironmentError("lock path got renamed after successful creation") - return lock_path - - -def register_cleanup_lock_removal(lock_path, register=atexit.register): - """registers a cleanup function for removing a lock, by default on atexit""" - pid = os.getpid() - - def cleanup_on_exit(lock_path=lock_path, original_pid=pid): - current_pid = os.getpid() - if current_pid != original_pid: - # fork - return - try: - lock_path.unlink() - except (OSError, IOError): - pass - - return register(cleanup_on_exit) - - -def maybe_delete_a_numbered_dir(path): - """removes a numbered directory if its lock can be obtained and it does not seem to be in use""" - lock_path = None - try: - lock_path = create_cleanup_lock(path) - parent = path.parent - - garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) - path.rename(garbage) +def find_prefixed(root, prefix): + """finds 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 + + +def extract_suffixes(iter, prefix): + """ + :param iter: iterator over path names + :param prefix: expected prefix of the path names + :returns: the parts of the paths following the prefix + """ + p_len = len(prefix) + for p in iter: + yield p.name[p_len:] + + +def find_suffixes(root, prefix): + """combines find_prefixes and extract_suffixes + """ + return extract_suffixes(find_prefixed(root, prefix), prefix) + + +def parse_num(maybe_num): + """parses number path suffixes, returns -1 on error""" + try: + return int(maybe_num) + except ValueError: + return -1 + + +if six.PY2: + + def _max(iterable, default): + """needed due to python2.7 lacking the default argument for max""" + return reduce(max, iterable, default) + + +else: + _max = max + + +def _force_symlink(root, target, link_to): + """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 testrun + + the presumption being thatin 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 + + +def make_numbered_dir(root, prefix): + """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 + max_existing = _max(map(parse_num, find_suffixes(root, prefix)), default=-1) + new_number = max_existing + 1 + new_path = root.joinpath("{}{}".format(prefix, new_number)) + try: + new_path.mkdir() + except Exception: + pass + else: + _force_symlink(root, prefix + "current", new_path) + return new_path + else: + raise EnvironmentError( + "could not create numbered dir with prefix " + "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) + ) + + +def create_cleanup_lock(p): + """crates 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) + except OSError as e: + if e.errno == errno.EEXIST: + six.raise_from( + EnvironmentError("cannot create lockfile in {path}".format(path=p)), e + ) + else: + raise + else: + pid = os.getpid() + spid = str(pid) + if not isinstance(spid, bytes): + spid = spid.encode("ascii") + os.write(fd, spid) + os.close(fd) + if not lock_path.is_file(): + raise EnvironmentError("lock path got renamed after successful creation") + return lock_path + + +def register_cleanup_lock_removal(lock_path, register=atexit.register): + """registers a cleanup function for removing a lock, by default on atexit""" + pid = os.getpid() + + def cleanup_on_exit(lock_path=lock_path, original_pid=pid): + current_pid = os.getpid() + if current_pid != original_pid: + # fork + return + try: + lock_path.unlink() + except (OSError, IOError): + pass + + return register(cleanup_on_exit) + + +def maybe_delete_a_numbered_dir(path): + """removes a numbered directory if its lock can be obtained and it does not seem to be in use""" + lock_path = None + try: + lock_path = create_cleanup_lock(path) + parent = path.parent + + garbage = parent.joinpath("garbage-{}".format(uuid.uuid4())) + path.rename(garbage) rm_rf(garbage) - except (OSError, EnvironmentError): - # 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() - except (OSError, IOError): - pass - - -def ensure_deletable(path, consider_lock_dead_if_created_before): - """checks if a lock exists and breaks it if its considered dead""" - if path.is_symlink(): - return False - lock = get_lock_path(path) - if not lock.exists(): - return True - try: - lock_time = lock.stat().st_mtime - except Exception: - return False - else: - if lock_time < consider_lock_dead_if_created_before: - lock.unlink() - return True - else: - return False - - -def try_cleanup(path, consider_lock_dead_if_created_before): - """tries 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) - - -def cleanup_candidates(root, prefix, keep): - """lists 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 - - -def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): - """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) - - -def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): - """creates a numbered dir with a cleanup lock and removes old ones""" - e = None - for i in range(10): - try: - p = make_numbered_dir(root, prefix) - 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 - cleanup_numbered_dir( - root=root, - prefix=prefix, - keep=keep, - consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, - ) - return p - assert e is not None - raise e - - -def resolve_from_str(input, root): - assert not isinstance(input, Path), "would break on py2" - root = Path(root) - input = expanduser(input) - input = expandvars(input) - if isabs(input): - return Path(input) - else: - return root.joinpath(input) - - -def fnmatch_ex(pattern, path): - """FNMatcher port 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: - "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: - name = six.text_type(path) - return fnmatch.fnmatch(name, pattern) - - -def parts(s): - parts = s.split(sep) - return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} + except (OSError, EnvironmentError): + # 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() + except (OSError, IOError): + pass + + +def ensure_deletable(path, consider_lock_dead_if_created_before): + """checks if a lock exists and breaks it if its considered dead""" + if path.is_symlink(): + return False + lock = get_lock_path(path) + if not lock.exists(): + return True + try: + lock_time = lock.stat().st_mtime + except Exception: + return False + else: + if lock_time < consider_lock_dead_if_created_before: + lock.unlink() + return True + else: + return False + + +def try_cleanup(path, consider_lock_dead_if_created_before): + """tries 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) + + +def cleanup_candidates(root, prefix, keep): + """lists 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 + + +def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): + """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) + + +def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): + """creates a numbered dir with a cleanup lock and removes old ones""" + e = None + for i in range(10): + try: + p = make_numbered_dir(root, prefix) + 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 + cleanup_numbered_dir( + root=root, + prefix=prefix, + keep=keep, + consider_lock_dead_if_created_before=consider_lock_dead_if_created_before, + ) + return p + assert e is not None + raise e + + +def resolve_from_str(input, root): + assert not isinstance(input, Path), "would break on py2" + root = Path(root) + input = expanduser(input) + input = expandvars(input) + if isabs(input): + return Path(input) + else: + return root.joinpath(input) + + +def fnmatch_ex(pattern, path): + """FNMatcher port 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: + "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: + name = six.text_type(path) + return fnmatch.fnmatch(name, pattern) + + +def parts(s): + parts = s.split(sep) + return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} diff --git a/contrib/python/pytest/py2/_pytest/pytester.py b/contrib/python/pytest/py2/_pytest/pytester.py index b907216770..f1d739c991 100644 --- a/contrib/python/pytest/py2/_pytest/pytester.py +++ b/contrib/python/pytest/py2/_pytest/pytester.py @@ -1,342 +1,342 @@ # -*- coding: utf-8 -*- -"""(disabled by default) support for testing pytest and pytest plugins.""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import codecs -import gc -import os -import platform -import re -import subprocess -import sys -import time -import traceback -from fnmatch import fnmatch -from weakref import WeakKeyDictionary - -import py -import six - -import pytest -from _pytest._code import Source +"""(disabled by default) support for testing pytest and pytest plugins.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import codecs +import gc +import os +import platform +import re +import subprocess +import sys +import time +import traceback +from fnmatch import fnmatch +from weakref import WeakKeyDictionary + +import py +import six + +import pytest +from _pytest._code import Source from _pytest._io.saferepr import saferepr -from _pytest.assertion.rewrite import AssertionRewritingHook -from _pytest.capture import MultiCapture -from _pytest.capture import SysCapture -from _pytest.compat import safe_str +from _pytest.assertion.rewrite import AssertionRewritingHook +from _pytest.capture import MultiCapture +from _pytest.capture import SysCapture +from _pytest.compat import safe_str from _pytest.compat import Sequence -from _pytest.main import EXIT_INTERRUPTED -from _pytest.main import EXIT_OK -from _pytest.main import Session +from _pytest.main import EXIT_INTERRUPTED +from _pytest.main import EXIT_OK +from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch -from _pytest.pathlib import Path - -IGNORE_PAM = [ # filenames added when obtaining details about the current user - u"/var/lib/sss/mc/passwd" -] - - -def pytest_addoption(parser): - 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): - if config.getvalue("lsof"): - checker = LsofFdLeakChecker() - if checker.matching_platform(): - config.pluginmanager.register(checker) - +from _pytest.pathlib import Path + +IGNORE_PAM = [ # filenames added when obtaining details about the current user + u"/var/lib/sss/mc/passwd" +] + + +def pytest_addoption(parser): + 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): + 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.", ) - -def raise_on_kwargs(kwargs): + +def raise_on_kwargs(kwargs): __tracebackhide__ = True if kwargs: # pragma: no branch raise TypeError( "Unexpected keyword arguments: {}".format(", ".join(sorted(kwargs))) ) - - -class LsofFdLeakChecker(object): - def get_open_files(self): - out = self._exec_lsof() - open_files = self._parse_lsof_output(out) - return open_files - - def _exec_lsof(self): - pid = os.getpid() + + +class LsofFdLeakChecker(object): + def get_open_files(self): + out = self._exec_lsof() + open_files = self._parse_lsof_output(out) + return open_files + + def _exec_lsof(self): + pid = os.getpid() # py3: use subprocess.DEVNULL directly. with open(os.devnull, "wb") as devnull: return subprocess.check_output( ("lsof", "-Ffn0", "-p", str(pid)), stderr=devnull ).decode() - - def _parse_lsof_output(self, out): - def isopen(line): - 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): - try: + + def _parse_lsof_output(self, out): + def isopen(line): + 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): + try: subprocess.check_output(("lsof", "-v")) except (OSError, subprocess.CalledProcessError): - return False - else: - return True - - @pytest.hookimpl(hookwrapper=True, tryfirst=True) - def pytest_runtest_protocol(self, item): - 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 = [] - error.append("***** %s FD leakage detected" % len(leaked_files)) - error.extend([str(f) for f in leaked_files]) - error.append("*** Before:") - error.extend([str(f) for f in lines1]) - error.append("*** After:") - error.extend([str(f) for f in lines2]) - error.append(error[0]) - error.append("*** function %s:%s: %s " % item.location) - error.append("See issue #2366") - item.warn(pytest.PytestWarning("\n".join(error))) - - -# used at least by pytest-xdist plugin - - -@pytest.fixture -def _pytest(request): - """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) - - -class PytestArg(object): - def __init__(self, request): - self.request = request - - def gethookrecorder(self, hook): - hookrecorder = HookRecorder(hook._pm) - self.request.addfinalizer(hookrecorder.finish_recording) - return hookrecorder - - -def get_public_names(values): - """Only return names from iterator values without a leading underscore.""" - return [x for x in values if x[0] != "_"] - - -class ParsedCall(object): - def __init__(self, name, kwargs): - self.__dict__.update(kwargs) - self._name = name - - def __repr__(self): - d = self.__dict__.copy() - del d["_name"] - return "<ParsedCall %r(**%r)>" % (self._name, d) - - -class HookRecorder(object): - """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): - self._pluginmanager = pluginmanager - self.calls = [] - - def before(hook_name, hook_impls, kwargs): - self.calls.append(ParsedCall(hook_name, kwargs)) - - def after(outcome, hook_name, hook_impls, kwargs): - pass - - self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) - - def finish_recording(self): - self._undo_wrapping() - - def getcalls(self, 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): - __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: - pytest.fail("could not find %r check %r" % (name, check)) - - def popcall(self, name): - __tracebackhide__ = True - for i, call in enumerate(self.calls): - if call._name == name: - del self.calls[i] - return call - lines = ["could not find call %r, in:" % (name,)] - lines.extend([" %s" % x for x in self.calls]) - pytest.fail("\n".join(lines)) - - def getcall(self, name): - values = self.getcalls(name) - assert len(values) == 1, (name, values) - return values[0] - - # functionality for test reports - - def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): - return [x.report for x in self.getcalls(names)] - - def matchreport( - self, - inamepart="", - names="pytest_runtest_logreport pytest_collectreport", - when=None, - ): - """return a testreport whose dotted import path matches""" - values = [] - for rep in self.getreports(names=names): + return False + else: + return True + + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_runtest_protocol(self, item): + 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 = [] + error.append("***** %s FD leakage detected" % len(leaked_files)) + error.extend([str(f) for f in leaked_files]) + error.append("*** Before:") + error.extend([str(f) for f in lines1]) + error.append("*** After:") + error.extend([str(f) for f in lines2]) + error.append(error[0]) + error.append("*** function %s:%s: %s " % item.location) + error.append("See issue #2366") + item.warn(pytest.PytestWarning("\n".join(error))) + + +# used at least by pytest-xdist plugin + + +@pytest.fixture +def _pytest(request): + """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) + + +class PytestArg(object): + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + + +def get_public_names(values): + """Only return names from iterator values without a leading underscore.""" + return [x for x in values if x[0] != "_"] + + +class ParsedCall(object): + def __init__(self, name, kwargs): + self.__dict__.update(kwargs) + self._name = name + + def __repr__(self): + d = self.__dict__.copy() + del d["_name"] + return "<ParsedCall %r(**%r)>" % (self._name, d) + + +class HookRecorder(object): + """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): + self._pluginmanager = pluginmanager + self.calls = [] + + def before(hook_name, hook_impls, kwargs): + self.calls.append(ParsedCall(hook_name, kwargs)) + + def after(outcome, hook_name, hook_impls, kwargs): + pass + + self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) + + def finish_recording(self): + self._undo_wrapping() + + def getcalls(self, 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): + __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: + pytest.fail("could not find %r check %r" % (name, check)) + + def popcall(self, name): + __tracebackhide__ = True + for i, call in enumerate(self.calls): + if call._name == name: + del self.calls[i] + return call + lines = ["could not find call %r, in:" % (name,)] + lines.extend([" %s" % x for x in self.calls]) + pytest.fail("\n".join(lines)) + + def getcall(self, name): + values = self.getcalls(name) + assert len(values) == 1, (name, values) + return values[0] + + # functionality for test reports + + def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): + return [x.report for x in self.getcalls(names)] + + def matchreport( + self, + inamepart="", + names="pytest_runtest_logreport pytest_collectreport", + when=None, + ): + """return a testreport whose dotted import path matches""" + 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( - "found 2 or more testreports matching %r: %s" % (inamepart, values) - ) - return values[0] - - def getfailures(self, names="pytest_runtest_logreport pytest_collectreport"): - return [rep for rep in self.getreports(names) if rep.failed] - - def getfailedcollections(self): - return self.getfailures("pytest_collectreport") - - def listoutcomes(self): - passed = [] - skipped = [] - failed = [] - for rep in self.getreports("pytest_collectreport pytest_runtest_logreport"): - if rep.passed: + 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: %s" % (inamepart, values) + ) + return values[0] + + def getfailures(self, names="pytest_runtest_logreport pytest_collectreport"): + return [rep for rep in self.getreports(names) if rep.failed] + + def getfailedcollections(self): + return self.getfailures("pytest_collectreport") + + def listoutcomes(self): + passed = [] + skipped = [] + failed = [] + for rep in self.getreports("pytest_collectreport pytest_runtest_logreport"): + if rep.passed: if rep.when == "call": - passed.append(rep) - elif rep.skipped: - skipped.append(rep) + passed.append(rep) + elif rep.skipped: + skipped.append(rep) else: assert rep.failed, "Unexpected outcome: {!r}".format(rep) - failed.append(rep) - return passed, skipped, failed - - def countoutcomes(self): - return [len(x) for x in self.listoutcomes()] - - def assertoutcome(self, passed=0, skipped=0, failed=0): - realpassed, realskipped, realfailed = self.listoutcomes() - assert passed == len(realpassed) - assert skipped == len(realskipped) - assert failed == len(realfailed) - - def clear(self): - self.calls[:] = [] - - -@pytest.fixture -def linecomp(request): - return LineComp() - - -@pytest.fixture(name="LineMatcher") -def LineMatcher_fixture(request): - return LineMatcher - - -@pytest.fixture -def testdir(request, tmpdir_factory): - return Testdir(request, tmpdir_factory) - - + failed.append(rep) + return passed, skipped, failed + + def countoutcomes(self): + return [len(x) for x in self.listoutcomes()] + + def assertoutcome(self, passed=0, skipped=0, failed=0): + realpassed, realskipped, realfailed = self.listoutcomes() + assert passed == len(realpassed) + assert skipped == len(realskipped) + assert failed == len(realfailed) + + def clear(self): + self.calls[:] = [] + + +@pytest.fixture +def linecomp(request): + return LineComp() + + +@pytest.fixture(name="LineMatcher") +def LineMatcher_fixture(request): + return LineMatcher + + +@pytest.fixture +def testdir(request, tmpdir_factory): + return Testdir(request, tmpdir_factory) + + @pytest.fixture def _sys_snapshot(): snappaths = SysPathsSnapshot() @@ -355,152 +355,152 @@ def _config_for_test(): config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles. -rex_outcome = re.compile(r"(\d+) ([\w-]+)") - - -class RunResult(object): - """The result of running a command. - - Attributes: - - :ret: the return value - :outlines: list of lines captured from stdout - :errlines: list of lines captures from stderr - :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to - reconstruct stdout or the commonly used ``stdout.fnmatch_lines()`` - method - :stderr: :py:class:`LineMatcher` of stderr - :duration: duration in seconds - - """ - - def __init__(self, ret, outlines, errlines, duration): - self.ret = ret - self.outlines = outlines - self.errlines = errlines - self.stdout = LineMatcher(outlines) - self.stderr = LineMatcher(errlines) - self.duration = duration - +rex_outcome = re.compile(r"(\d+) ([\w-]+)") + + +class RunResult(object): + """The result of running a command. + + Attributes: + + :ret: the return value + :outlines: list of lines captured from stdout + :errlines: list of lines captures from stderr + :stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to + reconstruct stdout or the commonly used ``stdout.fnmatch_lines()`` + method + :stderr: :py:class:`LineMatcher` of stderr + :duration: duration in seconds + + """ + + def __init__(self, ret, outlines, errlines, duration): + self.ret = ret + self.outlines = outlines + self.errlines = errlines + self.stdout = LineMatcher(outlines) + self.stderr = LineMatcher(errlines) + self.duration = duration + def __repr__(self): return ( "<RunResult ret=%r len(stdout.lines)=%d len(stderr.lines)=%d duration=%.2fs>" % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration) ) - def parseoutcomes(self): - """Return a dictionary of outcomestring->num from parsing the terminal - output that the test process produced. - - """ - for line in reversed(self.outlines): - if "seconds" in line: - outcomes = rex_outcome.findall(line) - if outcomes: - d = {} - for num, cat in outcomes: - d[cat] = int(num) - return d - raise ValueError("Pytest terminal report not found") - - def assert_outcomes( - self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0 - ): - """Assert that the specified outcomes appear with the respective - numbers (0 means it didn't occur) in the text output from a test run. - - """ - d = self.parseoutcomes() - obtained = { - "passed": d.get("passed", 0), - "skipped": d.get("skipped", 0), - "failed": d.get("failed", 0), - "error": d.get("error", 0), - "xpassed": d.get("xpassed", 0), - "xfailed": d.get("xfailed", 0), - } - expected = { - "passed": passed, - "skipped": skipped, - "failed": failed, - "error": error, - "xpassed": xpassed, - "xfailed": xfailed, - } - assert obtained == expected - - -class CwdSnapshot(object): - def __init__(self): - self.__saved = os.getcwd() - - def restore(self): - os.chdir(self.__saved) - - -class SysModulesSnapshot(object): - def __init__(self, preserve=None): - self.__preserve = preserve - self.__saved = dict(sys.modules) - - def restore(self): - 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(object): - def __init__(self): - self.__saved = list(sys.path), list(sys.meta_path) - - def restore(self): - sys.path[:], sys.meta_path[:] = self.__saved - - -class Testdir(object): - """Temporary test directory with tools to test/run pytest itself. - - This is based on the ``tmpdir`` fixture but provides a number of methods - which aid with testing pytest itself. Unless :py:meth:`chdir` is used all - methods will use :py:attr:`tmpdir` as their current working directory. - - Attributes: - - :tmpdir: The :py:class:`py.path.local` instance of the temporary directory. - - :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. - - """ - + def parseoutcomes(self): + """Return a dictionary of outcomestring->num from parsing the terminal + output that the test process produced. + + """ + for line in reversed(self.outlines): + if "seconds" in line: + outcomes = rex_outcome.findall(line) + if outcomes: + d = {} + for num, cat in outcomes: + d[cat] = int(num) + return d + raise ValueError("Pytest terminal report not found") + + def assert_outcomes( + self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0 + ): + """Assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run. + + """ + d = self.parseoutcomes() + obtained = { + "passed": d.get("passed", 0), + "skipped": d.get("skipped", 0), + "failed": d.get("failed", 0), + "error": d.get("error", 0), + "xpassed": d.get("xpassed", 0), + "xfailed": d.get("xfailed", 0), + } + expected = { + "passed": passed, + "skipped": skipped, + "failed": failed, + "error": error, + "xpassed": xpassed, + "xfailed": xfailed, + } + assert obtained == expected + + +class CwdSnapshot(object): + def __init__(self): + self.__saved = os.getcwd() + + def restore(self): + os.chdir(self.__saved) + + +class SysModulesSnapshot(object): + def __init__(self, preserve=None): + self.__preserve = preserve + self.__saved = dict(sys.modules) + + def restore(self): + 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(object): + def __init__(self): + self.__saved = list(sys.path), list(sys.meta_path) + + def restore(self): + sys.path[:], sys.meta_path[:] = self.__saved + + +class Testdir(object): + """Temporary test directory with tools to test/run pytest itself. + + This is based on the ``tmpdir`` fixture but provides a number of methods + which aid with testing pytest itself. Unless :py:meth:`chdir` is used all + methods will use :py:attr:`tmpdir` as their current working directory. + + Attributes: + + :tmpdir: The :py:class:`py.path.local` instance of the temporary directory. + + :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. + + """ + CLOSE_STDIN = object - class TimeoutExpired(Exception): - pass - - def __init__(self, request, tmpdir_factory): - self.request = request - self._mod_collections = WeakKeyDictionary() - name = request.function.__name__ - self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) - self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) - self.plugins = [] - 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) - method = self.request.config.getoption("--runpytest") - if method == "inprocess": - self._runpytest_method = self.runpytest_inprocess - elif method == "subprocess": - self._runpytest_method = self.runpytest_subprocess - + class TimeoutExpired(Exception): + pass + + def __init__(self, request, tmpdir_factory): + self.request = request + self._mod_collections = WeakKeyDictionary() + name = request.function.__name__ + self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) + self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) + self.plugins = [] + 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) + method = self.request.config.getoption("--runpytest") + if method == "inprocess": + self._runpytest_method = self.runpytest_inprocess + elif method == "subprocess": + self._runpytest_method = self.runpytest_subprocess + mp = self.monkeypatch = MonkeyPatch() mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot)) # Ensure no unexpected caching via tox. @@ -512,523 +512,523 @@ class Testdir(object): tmphome = str(self.tmpdir) self._env_run_update = {"HOME": tmphome, "USERPROFILE": tmphome} - def __repr__(self): - return "<Testdir %r>" % (self.tmpdir,) - - def __str__(self): - return str(self.tmpdir) - - def finalize(self): - """Clean up global state artifacts. - - 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() + def __repr__(self): + return "<Testdir %r>" % (self.tmpdir,) + + def __str__(self): + return str(self.tmpdir) + + def finalize(self): + """Clean up global state artifacts. + + 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() self.monkeypatch.undo() - - def __take_sys_modules_snapshot(self): - # some zope modules used by twisted-related tests keep internal state - # and can't be deleted; we had some trouble in the past with - # `zope.interface` for example - def preserve_module(name): - return name.startswith("zope") - - return SysModulesSnapshot(preserve=preserve_module) - - def make_hook_recorder(self, pluginmanager): - """Create a new :py:class:`HookRecorder` for a PluginManager.""" - pluginmanager.reprec = reprec = HookRecorder(pluginmanager) - self.request.addfinalizer(reprec.finish_recording) - return reprec - - def chdir(self): - """Cd into the temporary directory. - - This is done automatically upon instantiation. - - """ - self.tmpdir.chdir() - - def _makefile(self, ext, args, kwargs, encoding="utf-8"): - items = list(kwargs.items()) - - def to_text(s): - return s.decode(encoding) if isinstance(s, bytes) else six.text_type(s) - - if args: - source = u"\n".join(to_text(x) for x in args) - basename = self.request.function.__name__ - items.insert(0, (basename, source)) - - ret = None - for basename, value in items: - p = self.tmpdir.join(basename).new(ext=ext) - p.dirpath().ensure_dir() - source = Source(value) - source = u"\n".join(to_text(line) for line in source.lines) - p.write(source.strip().encode(encoding), "wb") - if ret is None: - ret = p - return ret - - def makefile(self, ext, *args, **kwargs): - r"""Create new file(s) in the testdir. - - :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. - :param list[str] args: All args will be treated as strings and joined using newlines. - The result will be written as contents to the file. The name of the - file will be based on the test function requesting this fixture. - :param kwargs: Each keyword is the name of a file, while the value of it will - be written as contents of the file. - - Examples: - - .. code-block:: python - - testdir.makefile(".txt", "line1", "line2") - - testdir.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") - - """ - return self._makefile(ext, args, kwargs) - - def makeconftest(self, source): - """Write a contest.py file with 'source' as contents.""" - return self.makepyfile(conftest=source) - - def makeini(self, source): - """Write a tox.ini file with 'source' as contents.""" - return self.makefile(".ini", tox=source) - - def getinicfg(self, source): - """Return the pytest section from the tox.ini config file.""" - p = self.makeini(source) - return py.iniconfig.IniConfig(p)["pytest"] - - def makepyfile(self, *args, **kwargs): - """Shortcut for .makefile() with a .py extension.""" - return self._makefile(".py", args, kwargs) - - def maketxtfile(self, *args, **kwargs): - """Shortcut for .makefile() with a .txt extension.""" - return self._makefile(".txt", args, kwargs) - - def syspathinsert(self, path=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.tmpdir - + + def __take_sys_modules_snapshot(self): + # some zope modules used by twisted-related tests keep internal state + # and can't be deleted; we had some trouble in the past with + # `zope.interface` for example + def preserve_module(name): + return name.startswith("zope") + + return SysModulesSnapshot(preserve=preserve_module) + + def make_hook_recorder(self, pluginmanager): + """Create a new :py:class:`HookRecorder` for a PluginManager.""" + pluginmanager.reprec = reprec = HookRecorder(pluginmanager) + self.request.addfinalizer(reprec.finish_recording) + return reprec + + def chdir(self): + """Cd into the temporary directory. + + This is done automatically upon instantiation. + + """ + self.tmpdir.chdir() + + def _makefile(self, ext, args, kwargs, encoding="utf-8"): + items = list(kwargs.items()) + + def to_text(s): + return s.decode(encoding) if isinstance(s, bytes) else six.text_type(s) + + if args: + source = u"\n".join(to_text(x) for x in args) + basename = self.request.function.__name__ + items.insert(0, (basename, source)) + + ret = None + for basename, value in items: + p = self.tmpdir.join(basename).new(ext=ext) + p.dirpath().ensure_dir() + source = Source(value) + source = u"\n".join(to_text(line) for line in source.lines) + p.write(source.strip().encode(encoding), "wb") + if ret is None: + ret = p + return ret + + def makefile(self, ext, *args, **kwargs): + r"""Create new file(s) in the testdir. + + :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. + :param list[str] args: All args will be treated as strings and joined using newlines. + The result will be written as contents to the file. The name of the + file will be based on the test function requesting this fixture. + :param kwargs: Each keyword is the name of a file, while the value of it will + be written as contents of the file. + + Examples: + + .. code-block:: python + + testdir.makefile(".txt", "line1", "line2") + + testdir.makefile(".ini", pytest="[pytest]\naddopts=-rs\n") + + """ + return self._makefile(ext, args, kwargs) + + def makeconftest(self, source): + """Write a contest.py file with 'source' as contents.""" + return self.makepyfile(conftest=source) + + def makeini(self, source): + """Write a tox.ini file with 'source' as contents.""" + return self.makefile(".ini", tox=source) + + def getinicfg(self, source): + """Return the pytest section from the tox.ini config file.""" + p = self.makeini(source) + return py.iniconfig.IniConfig(p)["pytest"] + + def makepyfile(self, *args, **kwargs): + """Shortcut for .makefile() with a .py extension.""" + return self._makefile(".py", args, kwargs) + + def maketxtfile(self, *args, **kwargs): + """Shortcut for .makefile() with a .txt extension.""" + return self._makefile(".txt", args, kwargs) + + def syspathinsert(self, path=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.tmpdir + self.monkeypatch.syspath_prepend(str(path)) - - def mkdir(self, name): - """Create a new (sub)directory.""" - return self.tmpdir.mkdir(name) - - def mkpydir(self, name): - """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.mkdir(name) - p.ensure("__init__.py") - return p - - def copy_example(self, name=None): - import warnings - from _pytest.warning_types import PYTESTER_COPY_EXAMPLE - - warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2) - 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") - example_dir = self.request.config.rootdir.join(example_dir) - - for extra_element in self.request.node.iter_markers("pytester_example_path"): - assert extra_element.args - example_dir = example_dir.join(*extra_element.args) - - if name is None: - func_name = self.request.function.__name__ - maybe_dir = example_dir / func_name - maybe_file = example_dir / (func_name + ".py") - - if maybe_dir.isdir(): - example_path = maybe_dir - elif maybe_file.isfile(): - example_path = maybe_file - else: - raise LookupError( - "{} cant be found as module or package in {}".format( + + def mkdir(self, name): + """Create a new (sub)directory.""" + return self.tmpdir.mkdir(name) + + def mkpydir(self, name): + """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.mkdir(name) + p.ensure("__init__.py") + return p + + def copy_example(self, name=None): + import warnings + from _pytest.warning_types import PYTESTER_COPY_EXAMPLE + + warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2) + 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") + example_dir = self.request.config.rootdir.join(example_dir) + + for extra_element in self.request.node.iter_markers("pytester_example_path"): + assert extra_element.args + example_dir = example_dir.join(*extra_element.args) + + if name is None: + func_name = self.request.function.__name__ + maybe_dir = example_dir / func_name + maybe_file = example_dir / (func_name + ".py") + + if maybe_dir.isdir(): + example_path = maybe_dir + elif maybe_file.isfile(): + example_path = maybe_file + else: + raise LookupError( + "{} cant be found as module or package in {}".format( func_name, example_dir.bestrelpath(self.request.config.rootdir) - ) - ) - else: - example_path = example_dir.join(name) - - if example_path.isdir() and not example_path.join("__init__.py").isfile(): - example_path.copy(self.tmpdir) - return self.tmpdir - elif example_path.isfile(): - result = self.tmpdir.join(example_path.basename) - example_path.copy(result) - return result - else: - raise LookupError( - 'example "{}" is not found as a file or directory'.format(example_path) - ) - - Session = Session - - def getnode(self, config, arg): - """Return the collection node of a file. - - :param config: :py:class:`_pytest.config.Config` instance, see - :py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the - configuration - - :param arg: a :py:class:`py.path.local` instance of the file - - """ - session = Session(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] - config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) - return res - - def getpathnode(self, path): - """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 path: a :py:class:`py.path.local` instance of the file - - """ - config = self.parseconfigure(path) - session = Session(config) - 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=EXIT_OK) - return res - - def genitems(self, colitems): - """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 = [] - for colitem in colitems: - result.extend(session.genitems(colitem)) - return result - - def runitem(self, source): - """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) - - def inline_runsource(self, source, *cmdlineargs): - """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 - - :return: :py:class:`HookRecorder` instance of the result - - """ - p = self.makepyfile(source) - values = list(cmdlineargs) + [p] - return self.inline_run(*values) - - def inline_genitems(self, *args): - """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, **kwargs): - """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` - + ) + ) + else: + example_path = example_dir.join(name) + + if example_path.isdir() and not example_path.join("__init__.py").isfile(): + example_path.copy(self.tmpdir) + return self.tmpdir + elif example_path.isfile(): + result = self.tmpdir.join(example_path.basename) + example_path.copy(result) + return result + else: + raise LookupError( + 'example "{}" is not found as a file or directory'.format(example_path) + ) + + Session = Session + + def getnode(self, config, arg): + """Return the collection node of a file. + + :param config: :py:class:`_pytest.config.Config` instance, see + :py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the + configuration + + :param arg: a :py:class:`py.path.local` instance of the file + + """ + session = Session(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] + config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK) + return res + + def getpathnode(self, path): + """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 path: a :py:class:`py.path.local` instance of the file + + """ + config = self.parseconfigure(path) + session = Session(config) + 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=EXIT_OK) + return res + + def genitems(self, colitems): + """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 = [] + for colitem in colitems: + result.extend(session.genitems(colitem)) + return result + + def runitem(self, source): + """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) + + def inline_runsource(self, source, *cmdlineargs): + """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 + + :return: :py:class:`HookRecorder` instance of the result + + """ + p = self.makepyfile(source) + values = list(cmdlineargs) + [p] + return self.inline_run(*values) + + def inline_genitems(self, *args): + """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, **kwargs): + """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: (keyword-only) extra plugin instances the - ``pytest.main()`` instance should use - - :return: a :py:class:`HookRecorder` instance + ``pytest.main()`` instance should use + + :return: a :py:class:`HookRecorder` instance """ plugins = kwargs.pop("plugins", []) no_reraise_ctrlc = kwargs.pop("no_reraise_ctrlc", None) raise_on_kwargs(kwargs) - - finalizers = [] - try: + + finalizers = [] + try: # Do not load user config (during runs only). mp_run = MonkeyPatch() for k, v in self._env_run_update.items(): mp_run.setenv(k, v) finalizers.append(mp_run.undo) - # When running pytest inline any plugins active in the main test - # process are already imported. So this disables the warning which - # will trigger to say they can no longer be rewritten, which is - # fine as they have already been rewritten. - orig_warn = AssertionRewritingHook._warn_already_imported - - def revert_warn_already_imported(): - AssertionRewritingHook._warn_already_imported = orig_warn - - finalizers.append(revert_warn_already_imported) - AssertionRewritingHook._warn_already_imported = lambda *a: None - - # 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(object): - def pytest_configure(x, config): - rec.append(self.make_hook_recorder(config.pluginmanager)) - - plugins.append(Collect()) - ret = pytest.main(list(args), plugins=plugins) - if len(rec) == 1: - reprec = rec.pop() - else: - - class reprec(object): - pass - - reprec.ret = ret - - # typically we reraise keyboard interrupts from the child run - # because it's our user requesting interruption of the testing + # When running pytest inline any plugins active in the main test + # process are already imported. So this disables the warning which + # will trigger to say they can no longer be rewritten, which is + # fine as they have already been rewritten. + orig_warn = AssertionRewritingHook._warn_already_imported + + def revert_warn_already_imported(): + AssertionRewritingHook._warn_already_imported = orig_warn + + finalizers.append(revert_warn_already_imported) + AssertionRewritingHook._warn_already_imported = lambda *a: None + + # 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(object): + def pytest_configure(x, config): + rec.append(self.make_hook_recorder(config.pluginmanager)) + + plugins.append(Collect()) + ret = pytest.main(list(args), plugins=plugins) + if len(rec) == 1: + reprec = rec.pop() + else: + + class reprec(object): + pass + + reprec.ret = ret + + # typically we reraise keyboard interrupts from the child run + # because it's our user requesting interruption of the testing if ret == EXIT_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() - - def runpytest_inprocess(self, *args, **kwargs): - """Return result of running pytest in-process, providing a similar - interface to what self.runpytest() provides. + 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, **kwargs): + """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() - now = time.time() - capture = MultiCapture(Capture=SysCapture) - capture.start_capturing() - try: - try: - reprec = self.inline_run(*args, **kwargs) - except SystemExit as e: - - class reprec(object): - ret = e.args[0] - - except Exception: - traceback.print_exc() - - class reprec(object): - ret = 3 - - finally: - out, err = capture.readouterr() - capture.stop_capturing() - sys.stdout.write(out) - sys.stderr.write(err) - - res = RunResult(reprec.ret, out.split("\n"), err.split("\n"), time.time() - now) - res.reprec = reprec - return res - - def runpytest(self, *args, **kwargs): - """Run pytest inline or in a subprocess, depending on the command line - option "--runpytest" and return a :py:class:`RunResult`. - - """ - args = self._ensure_basetemp(args) - return self._runpytest_method(*args, **kwargs) - - def _ensure_basetemp(self, args): - args = list(args) - for x in args: - if safe_str(x).startswith("--basetemp"): - break - else: - args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) - return args - - def parseconfig(self, *args): - """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. - - """ - args = self._ensure_basetemp(args) - - import _pytest.config - - config = _pytest.config._prepareconfig(args, self.plugins) - # 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 - - def parseconfigure(self, *args): - """Return a new pytest configured Config instance. - - This 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() - self.request.addfinalizer(config._ensure_unconfigure) - return config - - def getitem(self, source, funcname="test_func"): - """Return the test item for a test function. - - This 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. - - :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 - assert 0, "%r item not found in module:\n%s\nitems: %s" % ( - funcname, - source, - items, - ) - - def getitems(self, source): - """Return all test items collected from the module. - - This 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]) - - def getmodulecol(self, source, configargs=(), withinit=False): - """Return the module collection node for ``source``. - - This 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. - - :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): - path = self.tmpdir.join(str(source)) - assert not withinit, "not supported for paths" - else: - kw = {self.request.function.__name__: Source(source).strip()} - 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, name): - """Return the collection node for name from the module collection. - - This will search 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 - + self.syspathinsert() + now = time.time() + capture = MultiCapture(Capture=SysCapture) + capture.start_capturing() + try: + try: + reprec = self.inline_run(*args, **kwargs) + except SystemExit as e: + + class reprec(object): + ret = e.args[0] + + except Exception: + traceback.print_exc() + + class reprec(object): + ret = 3 + + finally: + out, err = capture.readouterr() + capture.stop_capturing() + sys.stdout.write(out) + sys.stderr.write(err) + + res = RunResult(reprec.ret, out.split("\n"), err.split("\n"), time.time() - now) + res.reprec = reprec + return res + + def runpytest(self, *args, **kwargs): + """Run pytest inline or in a subprocess, depending on the command line + option "--runpytest" and return a :py:class:`RunResult`. + + """ + args = self._ensure_basetemp(args) + return self._runpytest_method(*args, **kwargs) + + def _ensure_basetemp(self, args): + args = list(args) + for x in args: + if safe_str(x).startswith("--basetemp"): + break + else: + args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) + return args + + def parseconfig(self, *args): + """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. + + """ + args = self._ensure_basetemp(args) + + import _pytest.config + + config = _pytest.config._prepareconfig(args, self.plugins) + # 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 + + def parseconfigure(self, *args): + """Return a new pytest configured Config instance. + + This 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() + self.request.addfinalizer(config._ensure_unconfigure) + return config + + def getitem(self, source, funcname="test_func"): + """Return the test item for a test function. + + This 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. + + :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 + assert 0, "%r item not found in module:\n%s\nitems: %s" % ( + funcname, + source, + items, + ) + + def getitems(self, source): + """Return all test items collected from the module. + + This 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]) + + def getmodulecol(self, source, configargs=(), withinit=False): + """Return the module collection node for ``source``. + + This 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. + + :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): + path = self.tmpdir.join(str(source)) + assert not withinit, "not supported for paths" + else: + kw = {self.request.function.__name__: Source(source).strip()} + 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, name): + """Return the collection node for name from the module collection. + + This will search 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 + def popen( self, cmdargs, @@ -1037,377 +1037,377 @@ class Testdir(object): stdin=CLOSE_STDIN, **kw ): - """Invoke subprocess.Popen. - - This 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", "")]) - ) + """Invoke subprocess.Popen. + + This 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", "")]) + ) env.update(self._env_run_update) - kw["env"] = env - + kw["env"] = env + if stdin is Testdir.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 Testdir.CLOSE_STDIN: popen.stdin.close() elif isinstance(stdin, bytes): popen.stdin.write(stdin) - return popen - - def run(self, *cmdargs, **kwargs): - """Run a command with arguments. - - Run a process using subprocess.Popen saving the stdout and stderr. - - :param args: the sequence of arguments to pass to `subprocess.Popen()` - :param timeout: the period in seconds after which to timeout and raise - :py:class:`Testdir.TimeoutExpired` + return popen + + def run(self, *cmdargs, **kwargs): + """Run a command with arguments. + + Run a process using subprocess.Popen saving the stdout and stderr. + + :param args: the sequence of arguments to pass to `subprocess.Popen()` + :param timeout: the period in seconds after which to timeout and raise + :py:class:`Testdir.TimeoutExpired` :param stdin: optional standard input. Bytes are being send, closing the pipe, otherwise it is passed through to ``popen``. Defaults to ``CLOSE_STDIN``, which translates to using a pipe (``subprocess.PIPE``) that gets closed. - - Returns a :py:class:`RunResult`. - - """ - __tracebackhide__ = True - - timeout = kwargs.pop("timeout", None) + + Returns a :py:class:`RunResult`. + + """ + __tracebackhide__ = True + + timeout = kwargs.pop("timeout", None) stdin = kwargs.pop("stdin", Testdir.CLOSE_STDIN) - raise_on_kwargs(kwargs) - - cmdargs = [ - str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs - ] - p1 = self.tmpdir.join("stdout") - p2 = self.tmpdir.join("stderr") - print("running:", *cmdargs) - print(" in:", py.path.local()) - f1 = codecs.open(str(p1), "w", encoding="utf8") - f2 = codecs.open(str(p2), "w", encoding="utf8") - try: - now = time.time() - popen = self.popen( + raise_on_kwargs(kwargs) + + cmdargs = [ + str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs + ] + p1 = self.tmpdir.join("stdout") + p2 = self.tmpdir.join("stderr") + print("running:", *cmdargs) + print(" in:", py.path.local()) + f1 = codecs.open(str(p1), "w", encoding="utf8") + f2 = codecs.open(str(p2), "w", encoding="utf8") + try: + now = time.time() + popen = self.popen( cmdargs, stdin=stdin, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32"), - ) + ) if isinstance(stdin, bytes): popen.stdin.close() - - def handle_timeout(): - __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() + + def handle_timeout(): + __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() elif not six.PY2: - try: - ret = popen.wait(timeout) - except subprocess.TimeoutExpired: - handle_timeout() - else: - end = time.time() + timeout - - resolution = min(0.1, timeout / 10) - - while True: - ret = popen.poll() - if ret is not None: - break - - if time.time() > end: - handle_timeout() - - time.sleep(resolution) - finally: - f1.close() - f2.close() - f1 = codecs.open(str(p1), "r", encoding="utf8") - f2 = codecs.open(str(p2), "r", encoding="utf8") - try: - out = f1.read().splitlines() - err = f2.read().splitlines() - finally: - f1.close() - f2.close() - self._dump_lines(out, sys.stdout) - self._dump_lines(err, sys.stderr) - return RunResult(ret, out, err, time.time() - now) - - def _dump_lines(self, lines, fp): - try: - for line in lines: - print(line, file=fp) - except UnicodeEncodeError: - print("couldn't print to %s because of encoding" % (fp,)) - - def _getpytestargs(self): - return sys.executable, "-mpytest" - - def runpython(self, script): - """Run a python script using sys.executable as interpreter. - - Returns a :py:class:`RunResult`. - - """ - return self.run(sys.executable, script) - - def runpython_c(self, command): - """Run python -c "command", return a :py:class:`RunResult`.""" - return self.run(sys.executable, "-c", command) - - def runpytest_subprocess(self, *args, **kwargs): - """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:`Testdir.TimeoutExpired` - - Returns a :py:class:`RunResult`. - """ - __tracebackhide__ = True + try: + ret = popen.wait(timeout) + except subprocess.TimeoutExpired: + handle_timeout() + else: + end = time.time() + timeout + + resolution = min(0.1, timeout / 10) + + while True: + ret = popen.poll() + if ret is not None: + break + + if time.time() > end: + handle_timeout() + + time.sleep(resolution) + finally: + f1.close() + f2.close() + f1 = codecs.open(str(p1), "r", encoding="utf8") + f2 = codecs.open(str(p2), "r", encoding="utf8") + try: + out = f1.read().splitlines() + err = f2.read().splitlines() + finally: + f1.close() + f2.close() + self._dump_lines(out, sys.stdout) + self._dump_lines(err, sys.stderr) + return RunResult(ret, out, err, time.time() - now) + + def _dump_lines(self, lines, fp): + try: + for line in lines: + print(line, file=fp) + except UnicodeEncodeError: + print("couldn't print to %s because of encoding" % (fp,)) + + def _getpytestargs(self): + return sys.executable, "-mpytest" + + def runpython(self, script): + """Run a python script using sys.executable as interpreter. + + Returns a :py:class:`RunResult`. + + """ + return self.run(sys.executable, script) + + def runpython_c(self, command): + """Run python -c "command", return a :py:class:`RunResult`.""" + return self.run(sys.executable, "-c", command) + + def runpytest_subprocess(self, *args, **kwargs): + """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:`Testdir.TimeoutExpired` + + Returns a :py:class:`RunResult`. + """ + __tracebackhide__ = True timeout = kwargs.pop("timeout", None) raise_on_kwargs(kwargs) - - p = py.path.local.make_numbered_dir( - prefix="runpytest-", keep=None, rootdir=self.tmpdir - ) - 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 + + p = py.path.local.make_numbered_dir( + prefix="runpytest-", keep=None, rootdir=self.tmpdir + ) + 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, expect_timeout=10.0): - """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.tmpdir.mkdir("temp-pexpect") - invoke = " ".join(map(str, self._getpytestargs())) - cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) - return self.spawn(cmd, expect_timeout=expect_timeout) - - def spawn(self, cmd, expect_timeout=10.0): - """Run a command using pexpect. - - The pexpect child is returned. - - """ - pexpect = pytest.importorskip("pexpect", "3.0") - if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): - pytest.skip("pypy-64 bit not supported") - if sys.platform.startswith("freebsd"): - pytest.xfail("pexpect does not work reliably on freebsd") - logfile = self.tmpdir.join("spawn.out").open("wb") + + def spawn_pytest(self, string, expect_timeout=10.0): + """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.tmpdir.mkdir("temp-pexpect") + invoke = " ".join(map(str, self._getpytestargs())) + cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) + return self.spawn(cmd, expect_timeout=expect_timeout) + + def spawn(self, cmd, expect_timeout=10.0): + """Run a command using pexpect. + + The pexpect child is returned. + + """ + pexpect = pytest.importorskip("pexpect", "3.0") + if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): + pytest.skip("pypy-64 bit not supported") + if sys.platform.startswith("freebsd"): + pytest.xfail("pexpect does not work reliably on freebsd") + logfile = self.tmpdir.join("spawn.out").open("wb") # Do not load user config. env = os.environ.copy() env.update(self._env_run_update) child = pexpect.spawn(cmd, logfile=logfile, env=env) - self.request.addfinalizer(logfile.close) - child.timeout = expect_timeout - return child - - -def getdecoded(out): - try: - return out.decode("utf-8") - except UnicodeDecodeError: + self.request.addfinalizer(logfile.close) + child.timeout = expect_timeout + return child + + +def getdecoded(out): + try: + return out.decode("utf-8") + except UnicodeDecodeError: return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (saferepr(out),) - - -class LineComp(object): - def __init__(self): - self.stringio = py.io.TextIO() - - def assert_contains_lines(self, lines2): - """Assert that lines2 are contained (linearly) in lines1. - - Return a list of extralines found. - - """ - __tracebackhide__ = True - val = self.stringio.getvalue() - self.stringio.truncate(0) - self.stringio.seek(0) - lines1 = val.split("\n") - return LineMatcher(lines1).fnmatch_lines(lines2) - - -class LineMatcher(object): - """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): - self.lines = lines - self._log_output = [] - - def str(self): - """Return the entire original text.""" - return "\n".join(self.lines) - - def _getlines(self, lines2): - if isinstance(lines2, str): - lines2 = Source(lines2) - if isinstance(lines2, Source): - lines2 = lines2.strip().lines - return lines2 - - def fnmatch_lines_random(self, lines2): - """Check lines exist in the output using in any order. - - Lines are checked using ``fnmatch.fnmatch``. The argument is a list of - lines which have to occur in the output, in any order. - - """ - self._match_lines_random(lines2, fnmatch) - - def re_match_lines_random(self, lines2): - """Check lines exist in the output using ``re.match``, in any order. - - The argument is a list of lines which have to occur in the output, in - any order. - - """ - self._match_lines_random(lines2, lambda name, pat: re.match(pat, name)) - - def _match_lines_random(self, lines2, match_func): - """Check lines exist in the output. - - The argument is a list of lines which have to occur in the output, in - any order. Each line can contain glob whildcards. - - """ - 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: - self._log("line %r not found in output" % line) - raise ValueError(self._log_text) - - def get_lines_after(self, 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): + + +class LineComp(object): + def __init__(self): + self.stringio = py.io.TextIO() + + def assert_contains_lines(self, lines2): + """Assert that lines2 are contained (linearly) in lines1. + + Return a list of extralines found. + + """ + __tracebackhide__ = True + val = self.stringio.getvalue() + self.stringio.truncate(0) + self.stringio.seek(0) + lines1 = val.split("\n") + return LineMatcher(lines1).fnmatch_lines(lines2) + + +class LineMatcher(object): + """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): + self.lines = lines + self._log_output = [] + + def str(self): + """Return the entire original text.""" + return "\n".join(self.lines) + + def _getlines(self, lines2): + if isinstance(lines2, str): + lines2 = Source(lines2) + if isinstance(lines2, Source): + lines2 = lines2.strip().lines + return lines2 + + def fnmatch_lines_random(self, lines2): + """Check lines exist in the output using in any order. + + Lines are checked using ``fnmatch.fnmatch``. The argument is a list of + lines which have to occur in the output, in any order. + + """ + self._match_lines_random(lines2, fnmatch) + + def re_match_lines_random(self, lines2): + """Check lines exist in the output using ``re.match``, in any order. + + The argument is a list of lines which have to occur in the output, in + any order. + + """ + self._match_lines_random(lines2, lambda name, pat: re.match(pat, name)) + + def _match_lines_random(self, lines2, match_func): + """Check lines exist in the output. + + The argument is a list of lines which have to occur in the output, in + any order. Each line can contain glob whildcards. + + """ + 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: + self._log("line %r not found in output" % line) + raise ValueError(self._log_text) + + def get_lines_after(self, 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): self._log_output.append(" ".join(str(x) for x in args)) - - @property - def _log_text(self): - return "\n".join(self._log_output) - - def fnmatch_lines(self, lines2): - """Search captured text for matching lines using ``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 - matches and non-matches are also printed on stdout. - - """ - __tracebackhide__ = True - self._match_lines(lines2, fnmatch, "fnmatch") - - def re_match_lines(self, lines2): - """Search captured text for matching lines using ``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 matches and non-matches are also printed on stdout. - - """ - __tracebackhide__ = True - self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match") - - def _match_lines(self, lines2, match_func, match_nickname): - """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. - - :param list[str] lines2: list of string patterns to match. The actual - format depends on ``match_func`` - :param match_func: a callable ``match_func(line, pattern)`` where line - is the captured line from stdout/stderr and pattern is the matching - pattern - :param str match_nickname: the nickname for the match function that - will be logged to stdout when a match occurs - - """ + + @property + def _log_text(self): + return "\n".join(self._log_output) + + def fnmatch_lines(self, lines2): + """Search captured text for matching lines using ``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 + matches and non-matches are also printed on stdout. + + """ + __tracebackhide__ = True + self._match_lines(lines2, fnmatch, "fnmatch") + + def re_match_lines(self, lines2): + """Search captured text for matching lines using ``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 matches and non-matches are also printed on stdout. + + """ + __tracebackhide__ = True + self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match") + + def _match_lines(self, lines2, match_func, match_nickname): + """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``. + + :param list[str] lines2: list of string patterns to match. The actual + format depends on ``match_func`` + :param match_func: a callable ``match_func(line, pattern)`` where line + is the captured line from stdout/stderr and pattern is the matching + pattern + :param str match_nickname: the nickname for the match function that + will be logged to stdout when a match occurs + + """ assert isinstance(lines2, Sequence) - lines2 = self._getlines(lines2) - lines1 = self.lines[:] - nextline = None - extralines = [] - __tracebackhide__ = True - for line in lines2: - nomatchprinted = False - while lines1: - nextline = lines1.pop(0) - if line == nextline: - self._log("exact match:", repr(line)) - break - elif match_func(nextline, line): - self._log("%s:" % match_nickname, repr(line)) - self._log(" with:", repr(nextline)) - break - else: - if not nomatchprinted: - self._log("nomatch:", repr(line)) - nomatchprinted = True - self._log(" and:", repr(nextline)) - extralines.append(nextline) - else: - self._log("remains unmatched: %r" % (line,)) - pytest.fail(self._log_text) + lines2 = self._getlines(lines2) + lines1 = self.lines[:] + nextline = None + extralines = [] + __tracebackhide__ = True + for line in lines2: + nomatchprinted = False + while lines1: + nextline = lines1.pop(0) + if line == nextline: + self._log("exact match:", repr(line)) + break + elif match_func(nextline, line): + self._log("%s:" % match_nickname, repr(line)) + self._log(" with:", repr(nextline)) + break + else: + if not nomatchprinted: + self._log("nomatch:", repr(line)) + nomatchprinted = True + self._log(" and:", repr(nextline)) + extralines.append(nextline) + else: + self._log("remains unmatched: %r" % (line,)) + pytest.fail(self._log_text) diff --git a/contrib/python/pytest/py2/_pytest/python.py b/contrib/python/pytest/py2/_pytest/python.py index d8e11024ae..f7c368b0c4 100644 --- a/contrib/python/pytest/py2/_pytest/python.py +++ b/contrib/python/pytest/py2/_pytest/python.py @@ -1,103 +1,103 @@ # -*- coding: utf-8 -*- -""" Python test discovery, setup and run of test functions. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import collections -import fnmatch -import inspect -import os -import sys -import warnings +""" Python test discovery, setup and run of test functions. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import collections +import fnmatch +import inspect +import os +import sys +import warnings from functools import partial -from textwrap import dedent - -import py -import six - -import _pytest -from _pytest import deprecated -from _pytest import fixtures -from _pytest import nodes -from _pytest._code import filter_traceback -from _pytest.compat import ascii_escaped -from _pytest.compat import enum -from _pytest.compat import get_default_arg_names -from _pytest.compat import get_real_func -from _pytest.compat import getfslineno -from _pytest.compat import getimfunc -from _pytest.compat import getlocation -from _pytest.compat import is_generator -from _pytest.compat import isclass -from _pytest.compat import isfunction -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 safe_str -from _pytest.compat import STRING_TYPES -from _pytest.config import hookimpl -from _pytest.main import FSHookProxy +from textwrap import dedent + +import py +import six + +import _pytest +from _pytest import deprecated +from _pytest import fixtures +from _pytest import nodes +from _pytest._code import filter_traceback +from _pytest.compat import ascii_escaped +from _pytest.compat import enum +from _pytest.compat import get_default_arg_names +from _pytest.compat import get_real_func +from _pytest.compat import getfslineno +from _pytest.compat import getimfunc +from _pytest.compat import getlocation +from _pytest.compat import is_generator +from _pytest.compat import isclass +from _pytest.compat import isfunction +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 safe_str +from _pytest.compat import STRING_TYPES +from _pytest.config import hookimpl +from _pytest.main import FSHookProxy from _pytest.mark import MARK_GEN -from _pytest.mark.structures import get_unpacked_marks -from _pytest.mark.structures import normalize_mark_list -from _pytest.outcomes import fail +from _pytest.mark.structures import get_unpacked_marks +from _pytest.mark.structures import normalize_mark_list +from _pytest.outcomes import fail from _pytest.outcomes import skip -from _pytest.pathlib import parts +from _pytest.pathlib import parts from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning - - -def pyobj_property(name): - def get(self): - node = self.getparent(getattr(__import__("pytest"), name)) - if node is not None: - return node.obj - - doc = "python %s object this node was collected from (can be None)." % ( - name.lower(), - ) - return property(get, None, None, doc) - - -def pytest_addoption(parser): - 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", + + +def pyobj_property(name): + def get(self): + node = self.getparent(getattr(__import__("pytest"), name)) + if node is not None: + return node.obj + + doc = "python %s object this node was collected from (can be None)." % ( + name.lower(), + ) + return property(get, None, None, doc) + + +def pytest_addoption(parser): + 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", @@ -105,61 +105,61 @@ def pytest_addoption(parser): help="disable string escape non-ascii characters, might cause unwanted " "side effects(use at your own risk)", ) - - group.addoption( - "--import-mode", - default="prepend", - choices=["prepend", "append"], - dest="importmode", - help="prepend/append to sys.path when importing test modules, " - "default is to prepend.", - ) - - -def pytest_cmdline_main(config): - if config.option.showfixtures: - showfixtures(config) - return 0 - if config.option.show_fixtures_per_test: - show_fixtures_per_test(config) - return 0 - - -def pytest_generate_tests(metafunc): - # those alternative spellings are common - raise a specific error to alert - # the user - alt_spellings = ["parameterize", "parametrise", "parameterise"] + + group.addoption( + "--import-mode", + default="prepend", + choices=["prepend", "append"], + dest="importmode", + help="prepend/append to sys.path when importing test modules, " + "default is to prepend.", + ) + + +def pytest_cmdline_main(config): + if config.option.showfixtures: + showfixtures(config) + return 0 + if config.option.show_fixtures_per_test: + show_fixtures_per_test(config) + return 0 + + +def pytest_generate_tests(metafunc): + # those alternative spellings are common - raise a specific error to alert + # the user + alt_spellings = ["parameterize", "parametrise", "parameterise"] for mark_name in alt_spellings: if metafunc.definition.get_closest_marker(mark_name): - msg = "{0} has '{1}' mark, spelling should be 'parametrize'" + msg = "{0} has '{1}' mark, spelling should be 'parametrize'" fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False) - for marker in metafunc.definition.iter_markers(name="parametrize"): - metafunc.parametrize(*marker.args, **marker.kwargs) - - -def pytest_configure(config): - 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/latest/parametrize.html for more info " - "and examples.", - ) - config.addinivalue_line( - "markers", - "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " - "all of the specified fixtures. see " - "https://docs.pytest.org/en/latest/fixture.html#usefixtures ", - ) - - -@hookimpl(trylast=True) -def pytest_pyfunc_call(pyfuncitem): - testfunction = pyfuncitem.obj + for marker in metafunc.definition.iter_markers(name="parametrize"): + metafunc.parametrize(*marker.args, **marker.kwargs) + + +def pytest_configure(config): + 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/latest/parametrize.html for more info " + "and examples.", + ) + config.addinivalue_line( + "markers", + "usefixtures(fixturename1, fixturename2, ...): mark tests as needing " + "all of the specified fixtures. see " + "https://docs.pytest.org/en/latest/fixture.html#usefixtures ", + ) + + +@hookimpl(trylast=True) +def pytest_pyfunc_call(pyfuncitem): + testfunction = pyfuncitem.obj iscoroutinefunction = getattr(inspect, "iscoroutinefunction", None) if iscoroutinefunction is not None and iscoroutinefunction(testfunction): msg = "Coroutine functions are not natively supported and have been skipped.\n" @@ -172,85 +172,85 @@ def pytest_pyfunc_call(pyfuncitem): funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} testfunction(**testargs) - return True - - -def pytest_collect_file(path, parent): - 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 - ihook = parent.session.gethookproxy(path) - return ihook.pytest_pycollect_makemodule(path=path, parent=parent) - - -def path_matches_patterns(path, patterns): - """Returns True if the given py.path.local matches one of the patterns in the list of globs given""" - return any(path.fnmatch(pattern) for pattern in patterns) - - -def pytest_pycollect_makemodule(path, parent): - if path.basename == "__init__.py": - return Package(path, parent) - return Module(path, parent) - - -@hookimpl(hookwrapper=True) -def pytest_pycollect_makeitem(collector, name, obj): - outcome = yield - res = outcome.get_result() - if res is not None: - return - # nothing was collected elsewhere, let's do it here - if safe_isclass(obj): - if collector.istestclass(obj, name): - outcome.force_result(Class(name, parent=collector)) - 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 - # or a funtools.wrapped. - # We musn't if it's been wrapped with mock.patch (python 2 only) - if not (isfunction(obj) or isfunction(get_real_func(obj))): - filename, lineno = getfslineno(obj) - warnings.warn_explicit( + return True + + +def pytest_collect_file(path, parent): + 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 + ihook = parent.session.gethookproxy(path) + return ihook.pytest_pycollect_makemodule(path=path, parent=parent) + + +def path_matches_patterns(path, patterns): + """Returns True if the given py.path.local matches one of the patterns in the list of globs given""" + return any(path.fnmatch(pattern) for pattern in patterns) + + +def pytest_pycollect_makemodule(path, parent): + if path.basename == "__init__.py": + return Package(path, parent) + return Module(path, parent) + + +@hookimpl(hookwrapper=True) +def pytest_pycollect_makeitem(collector, name, obj): + outcome = yield + res = outcome.get_result() + if res is not None: + return + # nothing was collected elsewhere, let's do it here + if safe_isclass(obj): + if collector.istestclass(obj, name): + outcome.force_result(Class(name, parent=collector)) + 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 + # or a funtools.wrapped. + # We musn't if it's been wrapped with mock.patch (python 2 only) + if not (isfunction(obj) or isfunction(get_real_func(obj))): + 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(name, parent=collector) reason = deprecated.YIELD_TESTS.format(name=name) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) res.warn(PytestCollectionWarning(reason)) - else: - res = list(collector._genfunctions(name, obj)) - outcome.force_result(res) - - -def pytest_make_parametrize_id(config, val, argname=None): - return None - - -class PyobjContext(object): - module = pyobj_property("Module") - cls = pyobj_property("Class") - instance = pyobj_property("Instance") - - -class PyobjMixin(PyobjContext): - _ALLOW_MARKERS = True - - def __init__(self, *k, **kw): - super(PyobjMixin, self).__init__(*k, **kw) - + else: + res = list(collector._genfunctions(name, obj)) + outcome.force_result(res) + + +def pytest_make_parametrize_id(config, val, argname=None): + return None + + +class PyobjContext(object): + module = pyobj_property("Module") + cls = pyobj_property("Class") + instance = pyobj_property("Instance") + + +class PyobjMixin(PyobjContext): + _ALLOW_MARKERS = True + + def __init__(self, *k, **kw): + super(PyobjMixin, self).__init__(*k, **kw) + @property def obj(self): """Underlying Python object.""" @@ -262,189 +262,189 @@ class PyobjMixin(PyobjContext): 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): """Gets the underlying Python object. May be overwritten by subclasses.""" - return getattr(self.parent.obj, self.name) - - def getmodpath(self, stopatmodule=True, includemodule=False): - """ 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() - s = ".".join(parts) - return s.replace(".[", "[") - - def reportinfo(self): - # XXX caching? - obj = self.obj - compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) - if isinstance(compat_co_firstlineno, int): - # nose compatibility - fspath = sys.modules[obj.__module__].__file__ - if fspath.endswith(".pyc"): - fspath = fspath[:-1] - lineno = compat_co_firstlineno - else: - fspath, lineno = getfslineno(obj) - modpath = self.getmodpath() - assert isinstance(lineno, int) - return fspath, lineno, modpath - - -class PyCollector(PyobjMixin, nodes.Collector): - def funcnamefilter(self, name): - return self._matches_prefix_or_glob_option("python_functions", name) - - def isnosetest(self, obj): - """ 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 - - def classnamefilter(self, name): - return self._matches_prefix_or_glob_option("python_classes", name) - - def istestfunction(self, obj, name): - if self.funcnamefilter(name) or self.isnosetest(obj): - if isinstance(obj, staticmethod): - # static methods 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 - - def istestclass(self, obj, name): - return self.classnamefilter(name) or self.isnosetest(obj) - - def _matches_prefix_or_glob_option(self, option_name, name): - """ - checks 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 - # check that name looks like a glob-string before calling fnmatch - # 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 - - def collect(self): - 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 inspect.getmro(self.obj.__class__): - dicts.append(basecls.__dict__) - seen = {} - values = [] - for dic in dicts: - for name, obj in list(dic.items()): - if name in seen: - continue - seen[name] = True - res = self._makeitem(name, obj) - if res is None: - continue - if not isinstance(res, list): - res = [res] - values.extend(res) - values.sort(key=lambda item: item.reportinfo()[:2]) - return values - - def _makeitem(self, name, obj): - # assert self.ihook.fspath == self.fspath, self - return self.ihook.pytest_pycollect_makeitem(collector=self, name=name, obj=obj) - - def _genfunctions(self, name, funcobj): - module = self.getparent(Module).obj - clscol = self.getparent(Class) - cls = clscol and clscol.obj or None - fm = self.session._fixturemanager - - definition = FunctionDefinition(name=name, parent=self, callobj=funcobj) - fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls) - - metafunc = Metafunc( - definition, fixtureinfo, self.config, cls=cls, module=module - ) - methods = [] - if hasattr(module, "pytest_generate_tests"): - methods.append(module.pytest_generate_tests) - if hasattr(cls, "pytest_generate_tests"): - methods.append(cls().pytest_generate_tests) - if methods: - self.ihook.pytest_generate_tests.call_extra( - methods, dict(metafunc=metafunc) - ) - else: - self.ihook.pytest_generate_tests(metafunc=metafunc) - - if not metafunc._calls: - yield Function(name, parent=self, fixtureinfo=fixtureinfo) - else: - # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs - 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: - subname = "%s[%s]" % (name, callspec.id) - yield Function( - name=subname, - parent=self, - 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 collect(self): + return getattr(self.parent.obj, self.name) + + def getmodpath(self, stopatmodule=True, includemodule=False): + """ 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() + s = ".".join(parts) + return s.replace(".[", "[") + + def reportinfo(self): + # XXX caching? + obj = self.obj + compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) + if isinstance(compat_co_firstlineno, int): + # nose compatibility + fspath = sys.modules[obj.__module__].__file__ + if fspath.endswith(".pyc"): + fspath = fspath[:-1] + lineno = compat_co_firstlineno + else: + fspath, lineno = getfslineno(obj) + modpath = self.getmodpath() + assert isinstance(lineno, int) + return fspath, lineno, modpath + + +class PyCollector(PyobjMixin, nodes.Collector): + def funcnamefilter(self, name): + return self._matches_prefix_or_glob_option("python_functions", name) + + def isnosetest(self, obj): + """ 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 + + def classnamefilter(self, name): + return self._matches_prefix_or_glob_option("python_classes", name) + + def istestfunction(self, obj, name): + if self.funcnamefilter(name) or self.isnosetest(obj): + if isinstance(obj, staticmethod): + # static methods 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 + + def istestclass(self, obj, name): + return self.classnamefilter(name) or self.isnosetest(obj) + + def _matches_prefix_or_glob_option(self, option_name, name): + """ + checks 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 + # check that name looks like a glob-string before calling fnmatch + # 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 + + def collect(self): + 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 inspect.getmro(self.obj.__class__): + dicts.append(basecls.__dict__) + seen = {} + values = [] + for dic in dicts: + for name, obj in list(dic.items()): + if name in seen: + continue + seen[name] = True + res = self._makeitem(name, obj) + if res is None: + continue + if not isinstance(res, list): + res = [res] + values.extend(res) + values.sort(key=lambda item: item.reportinfo()[:2]) + return values + + def _makeitem(self, name, obj): + # assert self.ihook.fspath == self.fspath, self + return self.ihook.pytest_pycollect_makeitem(collector=self, name=name, obj=obj) + + def _genfunctions(self, name, funcobj): + module = self.getparent(Module).obj + clscol = self.getparent(Class) + cls = clscol and clscol.obj or None + fm = self.session._fixturemanager + + definition = FunctionDefinition(name=name, parent=self, callobj=funcobj) + fixtureinfo = fm.getfixtureinfo(definition, funcobj, cls) + + metafunc = Metafunc( + definition, fixtureinfo, self.config, cls=cls, module=module + ) + methods = [] + if hasattr(module, "pytest_generate_tests"): + methods.append(module.pytest_generate_tests) + if hasattr(cls, "pytest_generate_tests"): + methods.append(cls().pytest_generate_tests) + if methods: + self.ihook.pytest_generate_tests.call_extra( + methods, dict(metafunc=metafunc) + ) + else: + self.ihook.pytest_generate_tests(metafunc=metafunc) + + if not metafunc._calls: + yield Function(name, parent=self, fixtureinfo=fixtureinfo) + else: + # add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs + 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: + subname = "%s[%s]" % (name, callspec.id) + yield Function( + name=subname, + parent=self, + 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 collect(self): self._inject_setup_module_fixture() self._inject_setup_function_fixture() - self.session._fixturemanager.parsefactories(self) - return super(Module, self).collect() - + self.session._fixturemanager.parsefactories(self) + return super(Module, self).collect() + def _inject_setup_module_fixture(self): """Injects a hidden autouse, module scoped fixture into the collected module object that invokes setUpModule/tearDownModule if either or both are available. @@ -500,68 +500,68 @@ class Module(nodes.File, PyCollector): self.obj.__pytest_setup_function = xunit_setup_function_fixture - def _importtestmodule(self): - # we assume we are only called once per module - importmode = self.config.getoption("--import-mode") - try: - mod = self.fspath.pyimport(ensuresyspath=importmode) - except SyntaxError: - raise self.CollectError( + def _importtestmodule(self): + # we assume we are only called once per module + importmode = self.config.getoption("--import-mode") + try: + mod = self.fspath.pyimport(ensuresyspath=importmode) + except SyntaxError: + raise self.CollectError( _pytest._code.ExceptionInfo.from_current().getrepr(style="short") - ) - except self.fspath.ImportMismatchError: - e = sys.exc_info()[1] - 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 - ) - except ImportError: - from _pytest._code.code import ExceptionInfo - + ) + except self.fspath.ImportMismatchError: + e = sys.exc_info()[1] + 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 + ) + except ImportError: + from _pytest._code.code import ExceptionInfo + 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() - ) - formatted_tb = safe_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) - ) - except _pytest.runner.Skipped 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}." - ) - self.config.pluginmanager.consider_module(mod) - return mod - - -class Package(Module): - def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): - session = parent.session - nodes.FSCollector.__init__( - self, fspath, parent=parent, config=config, session=session, nodeid=nodeid - ) - self.name = fspath.dirname - self.trace = session.trace - self._norecursepatterns = session._norecursepatterns - self.fspath = fspath - + 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 = safe_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) + ) + except _pytest.runner.Skipped 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}." + ) + self.config.pluginmanager.consider_module(mod) + return mod + + +class Package(Module): + def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): + session = parent.session + nodes.FSCollector.__init__( + self, fspath, parent=parent, config=config, session=session, nodeid=nodeid + ) + self.name = fspath.dirname + self.trace = session.trace + self._norecursepatterns = session._norecursepatterns + self.fspath = fspath + def setup(self): # not using fixtures to call setup_module here because autouse fixtures # from packages are not called automatically (#4085) @@ -578,84 +578,84 @@ class Package(Module): func = partial(_call_with_optional_argument, teardown_module, self.obj) self.addfinalizer(func) - def _recurse(self, dirpath): - if dirpath.basename == "__pycache__": - return False - ihook = self.gethookproxy(dirpath.dirpath()) - if ihook.pytest_ignore_collect(path=dirpath, config=self.config): - return - for pat in self._norecursepatterns: - if dirpath.check(fnmatch=pat): - return False - ihook = self.gethookproxy(dirpath) - ihook.pytest_collect_directory(path=dirpath, parent=self) - return True - - def gethookproxy(self, fspath): - # check if we have the common case of running - # hooks with all conftest.py filesall conftest.py - pm = self.config.pluginmanager - my_conftestmodules = pm._getconftestmodules(fspath) - remove_mods = pm._conftest_plugins.difference(my_conftestmodules) - if remove_mods: - # one or more conftests are not in use at this fspath - proxy = FSHookProxy(fspath, pm, remove_mods) - else: - # all plugis are active for this fspath - proxy = self.config.hook - return proxy - - def _collectfile(self, path, handle_dupes=True): + def _recurse(self, dirpath): + if dirpath.basename == "__pycache__": + return False + ihook = self.gethookproxy(dirpath.dirpath()) + if ihook.pytest_ignore_collect(path=dirpath, config=self.config): + return + for pat in self._norecursepatterns: + if dirpath.check(fnmatch=pat): + return False + ihook = self.gethookproxy(dirpath) + ihook.pytest_collect_directory(path=dirpath, parent=self) + return True + + def gethookproxy(self, fspath): + # check if we have the common case of running + # hooks with all conftest.py filesall conftest.py + pm = self.config.pluginmanager + my_conftestmodules = pm._getconftestmodules(fspath) + remove_mods = pm._conftest_plugins.difference(my_conftestmodules) + if remove_mods: + # one or more conftests are not in use at this fspath + proxy = FSHookProxy(fspath, pm, remove_mods) + else: + # all plugis are active for this fspath + proxy = self.config.hook + return proxy + + def _collectfile(self, path, handle_dupes=True): assert path.isfile(), "%r is not a file (isdir=%r, exists=%r, islink=%r)" % ( path, path.isdir(), path.exists(), path.islink(), ) - ihook = self.gethookproxy(path) - if not self.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): - return () - - if handle_dupes: - keepduplicates = self.config.getoption("keepduplicates") - if not keepduplicates: - duplicate_paths = self.config.pluginmanager._duplicatepaths - if path in duplicate_paths: - return () - else: - duplicate_paths.add(path) - - if self.fspath == path: # __init__.py - return [self] - - return ihook.pytest_collect_file(path=path, parent=self) - - def isinitpath(self, path): - return path in self.session._initialpaths - - def collect(self): - 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(init_module, self) - pkg_prefixes = set() - for path in this_path.visit(rec=self._recurse, bf=True, sort=True): - # We will visit our own __init__.py file, in which case we skip it. + ihook = self.gethookproxy(path) + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + + if handle_dupes: + keepduplicates = self.config.getoption("keepduplicates") + if not keepduplicates: + duplicate_paths = self.config.pluginmanager._duplicatepaths + if path in duplicate_paths: + return () + else: + duplicate_paths.add(path) + + if self.fspath == path: # __init__.py + return [self] + + return ihook.pytest_collect_file(path=path, parent=self) + + def isinitpath(self, path): + return path in self.session._initialpaths + + def collect(self): + 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(init_module, self) + pkg_prefixes = set() + for path in this_path.visit(rec=self._recurse, bf=True, sort=True): + # We will visit our own __init__.py file, in which case we skip it. is_file = path.isfile() if is_file: - if path.basename == "__init__.py" and path.dirpath() == this_path: - continue - - parts_ = parts(path.strpath) - if any( - pkg_prefix in parts_ and pkg_prefix.join("__init__.py") != path - for pkg_prefix in pkg_prefixes - ): - continue - + if path.basename == "__init__.py" and path.dirpath() == this_path: + continue + + parts_ = parts(path.strpath) + if any( + pkg_prefix in parts_ and pkg_prefix.join("__init__.py") != path + for pkg_prefix in pkg_prefixes + ): + continue + if is_file: for x in self._collectfile(path): yield x @@ -663,30 +663,30 @@ class Package(Module): # Broken symlink or invalid/missing file. continue elif path.join("__init__.py").check(file=1): - pkg_prefixes.add(path) - - -def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): - """ - Return a callable to perform xunit-style setup or teardown if - the function exists in the ``holder`` object. - The ``param_obj`` parameter is the parameter which will be passed to the function - when the callable is called without arguments, defaults to the ``holder`` object. - Return ``None`` if a suitable callable is not found. - """ + pkg_prefixes.add(path) + + +def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): + """ + Return a callable to perform xunit-style setup or teardown if + the function exists in the ``holder`` object. + The ``param_obj`` parameter is the parameter which will be passed to the function + when the callable is called without arguments, defaults to the ``holder`` object. + Return ``None`` if a suitable callable is not found. + """ # TODO: only needed because of Package! - param_obj = param_obj if param_obj is not None else holder + param_obj = param_obj if param_obj is not None else holder result = _get_non_fixture_func(holder, attr_name) - if result is not None: - arg_count = result.__code__.co_argcount - if inspect.ismethod(result): - arg_count -= 1 - if arg_count: - return lambda: result(param_obj) - else: - return result - - + if result is not None: + arg_count = result.__code__.co_argcount + if inspect.ismethod(result): + arg_count -= 1 + if arg_count: + return lambda: result(param_obj) + else: + return result + + def _call_with_optional_argument(func, arg): """Call the given function with the given argument if func accepts one argument, otherwise calls func without arguments""" @@ -700,49 +700,49 @@ def _call_with_optional_argument(func, arg): def _get_non_fixture_func(obj, name): - """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. - """ - meth = getattr(obj, name, None) - if fixtures.getfixturemarker(meth) is None: - return meth - - -class Class(PyCollector): - """ Collector for test methods. """ - - def collect(self): - if not safe_getattr(self.obj, "__test__", True): - return [] - if hasinit(self.obj): - self.warn( + """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. + """ + meth = getattr(obj, name, None) + if fixtures.getfixturemarker(meth) is None: + return meth + + +class Class(PyCollector): + """ Collector for test methods. """ + + def collect(self): + if not safe_getattr(self.obj, "__test__", True): + return [] + if hasinit(self.obj): + 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): - self.warn( + ) + ) + return [] + elif hasnew(self.obj): + 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(name="()", parent=self)] - + def _inject_setup_class_fixture(self): """Injects 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). """ @@ -789,378 +789,378 @@ class Class(PyCollector): self.obj.__pytest_setup_method = xunit_setup_method_fixture -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 - # can be removed at node structure reorganization time - - def _getobj(self): - return self.parent.obj() - - def collect(self): - self.session._fixturemanager.parsefactories(self) - return super(Instance, self).collect() - - def newinstance(self): - self.obj = self._getobj() - return self.obj - - -class FunctionMixin(PyobjMixin): - """ mixin for the code common to Function and Generator. - """ - - def setup(self): - """ perform setup for this test function. """ +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 + # can be removed at node structure reorganization time + + def _getobj(self): + return self.parent.obj() + + def collect(self): + self.session._fixturemanager.parsefactories(self) + return super(Instance, self).collect() + + def newinstance(self): + self.obj = self._getobj() + return self.obj + + +class FunctionMixin(PyobjMixin): + """ mixin for the code common to Function and Generator. + """ + + def setup(self): + """ perform setup for this test function. """ if isinstance(self.parent, Instance): self.parent.newinstance() - self.obj = self._getobj() - - def _prunetraceback(self, excinfo): + self.obj = self._getobj() + + def _prunetraceback(self, excinfo): if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): - code = _pytest._code.Code(get_real_func(self.obj)) - path, firstlineno = code.path, code.firstlineno - traceback = excinfo.traceback - ntraceback = traceback.cut(path=path, firstlineno=firstlineno) - if ntraceback == traceback: - ntraceback = ntraceback.cut(path=path) - if ntraceback == traceback: - 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 + code = _pytest._code.Code(get_real_func(self.obj)) + path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback + ntraceback = traceback.cut(path=path, firstlineno=firstlineno) + if ntraceback == traceback: + ntraceback = ntraceback.cut(path=path) + if ntraceback == traceback: + 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 if self.config.getoption("tbstyle", "auto") == "auto": - if len(excinfo.traceback) > 2: - for entry in excinfo.traceback[1:-1]: - entry.set_repr_style("short") - - def repr_failure(self, excinfo, outerr=None): - assert outerr is None, "XXX outerr usage is deprecated" + if len(excinfo.traceback) > 2: + for entry in excinfo.traceback[1:-1]: + entry.set_repr_style("short") + + def repr_failure(self, excinfo, outerr=None): + assert outerr is None, "XXX outerr usage is deprecated" style = self.config.getoption("tbstyle", "auto") - if style == "auto": - style = "long" - return self._repr_failure_py(excinfo, style=style) - - -def hasinit(obj): - init = getattr(obj, "__init__", None) - if init: - return init != object.__init__ - - -def hasnew(obj): - new = getattr(obj, "__new__", None) - if new: - return new != object.__new__ - - -class CallSpec2(object): - def __init__(self, metafunc): - self.metafunc = metafunc - self.funcargs = {} - self._idlist = [] - self.params = {} - self._globalid = NOTSET - self._globalparam = NOTSET - self._arg2scopenum = {} # used for sorting parametrized resources - self.marks = [] - self.indices = {} - - def copy(self): - 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) - cs._globalid = self._globalid - cs._globalparam = self._globalparam - return cs - - def _checkargnotcontained(self, arg): - if arg in self.params or arg in self.funcargs: - raise ValueError("duplicate %r" % (arg,)) - - def getparam(self, name): - try: - return self.params[name] - except KeyError: - if self._globalparam is NOTSET: - raise ValueError(name) - return self._globalparam - - @property - def id(self): + if style == "auto": + style = "long" + return self._repr_failure_py(excinfo, style=style) + + +def hasinit(obj): + init = getattr(obj, "__init__", None) + if init: + return init != object.__init__ + + +def hasnew(obj): + new = getattr(obj, "__new__", None) + if new: + return new != object.__new__ + + +class CallSpec2(object): + def __init__(self, metafunc): + self.metafunc = metafunc + self.funcargs = {} + self._idlist = [] + self.params = {} + self._globalid = NOTSET + self._globalparam = NOTSET + self._arg2scopenum = {} # used for sorting parametrized resources + self.marks = [] + self.indices = {} + + def copy(self): + 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) + cs._globalid = self._globalid + cs._globalparam = self._globalparam + return cs + + def _checkargnotcontained(self, arg): + if arg in self.params or arg in self.funcargs: + raise ValueError("duplicate %r" % (arg,)) + + def getparam(self, name): + try: + return self.params[name] + except KeyError: + if self._globalparam is NOTSET: + raise ValueError(name) + return self._globalparam + + @property + def id(self): return "-".join(map(safe_str, self._idlist)) - - def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index): - for arg, val in zip(argnames, valset): - self._checkargnotcontained(arg) - valtype_for_arg = valtypes[arg] - getattr(self, valtype_for_arg)[arg] = val - self.indices[arg] = param_index - self._arg2scopenum[arg] = scopenum - self._idlist.append(id) - self.marks.extend(normalize_mark_list(marks)) - - def setall(self, funcargs, id, param): - for x in funcargs: - self._checkargnotcontained(x) - self.funcargs.update(funcargs) - if id is not NOTSET: - self._idlist.append(id) - if param is not NOTSET: - assert self._globalparam is NOTSET - self._globalparam = param - for arg in funcargs: - self._arg2scopenum[arg] = fixtures.scopenum_function - - -class Metafunc(fixtures.FuncargnamesCompatAttr): - """ - Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. - They help to inspect a test function and to generate tests according to - test configuration or values specified in the class or module where a - test function is defined. - """ - - def __init__(self, definition, fixtureinfo, config, cls=None, module=None): - assert ( - isinstance(definition, FunctionDefinition) - or type(definition).__name__ == "DefinitionMock" - ) - self.definition = definition - - #: access to the :class:`_pytest.config.Config` object for the test session - self.config = config - - #: the module object where the test function is defined in. - self.module = module - - #: underlying python test function - self.function = definition.obj - - #: set of fixture names required by the test function - self.fixturenames = fixtureinfo.names_closure - - #: class object where the test function is defined in or ``None``. - self.cls = cls - - self._calls = [] - self._ids = set() - self._arg2fixturedefs = fixtureinfo.name2fixturedefs - - def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=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. - - :arg argnames: a comma-separated string denoting one or more argument - names, or a list/tuple of argument strings. - - :arg argvalues: The list of argvalues determines how often a - 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 respective - argname. - - :arg indirect: The list of argnames or boolean. A list of arguments' - names (subset of argnames). 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. - - :arg ids: list of string ids, or a callable. - If strings, each is corresponding to the argvalues so that they are - part of the test id. If None is given as id of specific test, the - automatically generated id for that argument will be used. - If callable, it should take one argument (a single argvalue) and return - a string or return None. If None, the automatically generated id for that - argument will be used. - If no ids are provided they will be generated automatically from - the argvalues. - - :arg 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 - from _pytest.mark import ParameterSet - - argnames, parameters = ParameterSet._for_parametrize( - argnames, - argvalues, - self.function, - self.config, - function_definition=self.definition, - ) - del argvalues - - 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) - - ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) - - scopenum = scope2index( - scope, descr="parametrize() call in {}".format(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 - # 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 - - def _resolve_arg_ids(self, argnames, ids, parameters, item): - """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given - 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 Item item: the item that generated this parametrized call. - :rtype: List[str] - :return: the list of ids for each argname given - """ + + def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index): + for arg, val in zip(argnames, valset): + self._checkargnotcontained(arg) + valtype_for_arg = valtypes[arg] + getattr(self, valtype_for_arg)[arg] = val + self.indices[arg] = param_index + self._arg2scopenum[arg] = scopenum + self._idlist.append(id) + self.marks.extend(normalize_mark_list(marks)) + + def setall(self, funcargs, id, param): + for x in funcargs: + self._checkargnotcontained(x) + self.funcargs.update(funcargs) + if id is not NOTSET: + self._idlist.append(id) + if param is not NOTSET: + assert self._globalparam is NOTSET + self._globalparam = param + for arg in funcargs: + self._arg2scopenum[arg] = fixtures.scopenum_function + + +class Metafunc(fixtures.FuncargnamesCompatAttr): + """ + Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. + They help to inspect a test function and to generate tests according to + test configuration or values specified in the class or module where a + test function is defined. + """ + + def __init__(self, definition, fixtureinfo, config, cls=None, module=None): + assert ( + isinstance(definition, FunctionDefinition) + or type(definition).__name__ == "DefinitionMock" + ) + self.definition = definition + + #: access to the :class:`_pytest.config.Config` object for the test session + self.config = config + + #: the module object where the test function is defined in. + self.module = module + + #: underlying python test function + self.function = definition.obj + + #: set of fixture names required by the test function + self.fixturenames = fixtureinfo.names_closure + + #: class object where the test function is defined in or ``None``. + self.cls = cls + + self._calls = [] + self._ids = set() + self._arg2fixturedefs = fixtureinfo.name2fixturedefs + + def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=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. + + :arg argnames: a comma-separated string denoting one or more argument + names, or a list/tuple of argument strings. + + :arg argvalues: The list of argvalues determines how often a + 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 respective + argname. + + :arg indirect: The list of argnames or boolean. A list of arguments' + names (subset of argnames). 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. + + :arg ids: list of string ids, or a callable. + If strings, each is corresponding to the argvalues so that they are + part of the test id. If None is given as id of specific test, the + automatically generated id for that argument will be used. + If callable, it should take one argument (a single argvalue) and return + a string or return None. If None, the automatically generated id for that + argument will be used. + If no ids are provided they will be generated automatically from + the argvalues. + + :arg 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 + from _pytest.mark import ParameterSet + + argnames, parameters = ParameterSet._for_parametrize( + argnames, + argvalues, + self.function, + self.config, + function_definition=self.definition, + ) + del argvalues + + 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) + + ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition) + + scopenum = scope2index( + scope, descr="parametrize() call in {}".format(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 + # 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 + + def _resolve_arg_ids(self, argnames, ids, parameters, item): + """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given + 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 Item item: the item that generated this parametrized call. + :rtype: List[str] + :return: the list of ids for each argname given + """ from _pytest._io.saferepr import saferepr - - idfn = None - if callable(ids): - idfn = ids - ids = None - if ids: - func_name = self.function.__name__ - if len(ids) != len(parameters): - msg = "In {}: {} parameter sets specified, with different number of ids: {}" - fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) - for id_value in ids: - if id_value is not None and not isinstance(id_value, six.string_types): - msg = "In {}: ids must be list of strings, found: {} (type: {!r})" - fail( - msg.format(func_name, saferepr(id_value), type(id_value)), - pytrace=False, - ) - ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) - return ids - - def _resolve_arg_value_types(self, argnames, indirect): - """Resolves 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 ``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. - """ - valtypes = {} - if indirect is True: - valtypes = dict.fromkeys(argnames, "params") - elif indirect is False: - valtypes = dict.fromkeys(argnames, "funcargs") - elif isinstance(indirect, (tuple, list)): - 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" - return valtypes - - def _validate_if_using_arg_names(self, argnames, indirect): - """ - 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 ``indirect`` parameter of ``parametrize()``. - :raise 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: - if isinstance(indirect, (tuple, list)): - name = "fixture" if arg in indirect else "argument" - else: - name = "fixture" if indirect else "argument" - fail( - "In {}: function uses no {} '{}'".format(func_name, name, arg), - pytrace=False, - ) - - -def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): - """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. - """ - from _pytest.fixtures import scopes - - if isinstance(indirect, (list, tuple)): - 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(scopes): - if scope in used_scopes: - return scope - - return "function" - - + + idfn = None + if callable(ids): + idfn = ids + ids = None + if ids: + func_name = self.function.__name__ + if len(ids) != len(parameters): + msg = "In {}: {} parameter sets specified, with different number of ids: {}" + fail(msg.format(func_name, len(parameters), len(ids)), pytrace=False) + for id_value in ids: + if id_value is not None and not isinstance(id_value, six.string_types): + msg = "In {}: ids must be list of strings, found: {} (type: {!r})" + fail( + msg.format(func_name, saferepr(id_value), type(id_value)), + pytrace=False, + ) + ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item) + return ids + + def _resolve_arg_value_types(self, argnames, indirect): + """Resolves 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 ``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. + """ + valtypes = {} + if indirect is True: + valtypes = dict.fromkeys(argnames, "params") + elif indirect is False: + valtypes = dict.fromkeys(argnames, "funcargs") + elif isinstance(indirect, (tuple, list)): + 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" + return valtypes + + def _validate_if_using_arg_names(self, argnames, indirect): + """ + 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 ``indirect`` parameter of ``parametrize()``. + :raise 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: + if isinstance(indirect, (tuple, list)): + name = "fixture" if arg in indirect else "argument" + else: + name = "fixture" if indirect else "argument" + fail( + "In {}: function uses no {} '{}'".format(func_name, name, arg), + pytrace=False, + ) + + +def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): + """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. + """ + from _pytest.fixtures import scopes + + if isinstance(indirect, (list, tuple)): + 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(scopes): + if scope in used_scopes: + return scope + + return "function" + + def _ascii_escaped_by_config(val, config): if config is None: escape_option = False @@ -1171,39 +1171,39 @@ def _ascii_escaped_by_config(val, config): return val if escape_option else ascii_escaped(val) -def _idval(val, argname, idx, idfn, item, config): - if idfn: - try: +def _idval(val, argname, idx, idfn, item, config): + if idfn: + try: generated_id = idfn(val) if generated_id is not None: val = generated_id - except Exception as e: - # See issue https://github.com/pytest-dev/pytest/issues/2169 + except Exception as e: + # See issue https://github.com/pytest-dev/pytest/issues/2169 msg = "{}: error raised while trying to determine id of parameter '{}' at position {}\n" msg = msg.format(item.nodeid, argname, idx) # we only append the exception type and message because on Python 2 reraise does nothing - msg += " {}: {}\n".format(type(e).__name__, e) + msg += " {}: {}\n".format(type(e).__name__, e) six.raise_from(ValueError(msg), e) elif config: - hook_id = config.hook.pytest_make_parametrize_id( - config=config, val=val, argname=argname - ) - if hook_id: - return hook_id - - if isinstance(val, STRING_TYPES): + hook_id = config.hook.pytest_make_parametrize_id( + config=config, val=val, argname=argname + ) + 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) - elif enum is not None and isinstance(val, enum.Enum): - return str(val) - elif (isclass(val) or isfunction(val)) and hasattr(val, "__name__"): - return val.__name__ - return str(argname) + str(idx) - - + return str(val) + elif isinstance(val, REGEX_TYPE): + return ascii_escaped(val.pattern) + elif enum is not None and isinstance(val, enum.Enum): + return str(val) + elif (isclass(val) or isfunction(val)) and hasattr(val, "__name__"): + return val.__name__ + return str(argname) + str(idx) + + def limit_idval(limit): import functools @@ -1231,212 +1231,212 @@ def limit_idval(limit): # XXX limit testnames in the name of sanity and readability @limit_idval(limit=500) -def _idvalset(idx, parameterset, argnames, idfn, ids, item, config): - if parameterset.id is not None: - return parameterset.id - if ids is None or (idx >= len(ids) or ids[idx] is None): - this_id = [ - _idval(val, argname, idx, idfn, item=item, config=config) - for val, argname in zip(parameterset.values, argnames) - ] - return "-".join(this_id) - else: +def _idvalset(idx, parameterset, argnames, idfn, ids, item, config): + if parameterset.id is not None: + return parameterset.id + if ids is None or (idx >= len(ids) or ids[idx] is None): + this_id = [ + _idval(val, argname, idx, idfn, item=item, config=config) + for val, argname in zip(parameterset.values, argnames) + ] + return "-".join(this_id) + else: return _ascii_escaped_by_config(ids[idx], config) - - -def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None): - ids = [ - _idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item) - for valindex, parameterset in enumerate(parametersets) - ] - if len(set(ids)) != len(ids): - # The ids are not unique - duplicates = [testid for testid in ids if ids.count(testid) > 1] - counters = collections.defaultdict(lambda: 0) - for index, testid in enumerate(ids): - if testid in duplicates: - ids[index] = testid + str(counters[testid]) - counters[testid] += 1 - return 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, session): - 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, curdir) - return curdir.bestrelpath(loc) - - def write_fixture(fixture_def): - argname = fixture_def.argname - if verbose <= 0 and argname.startswith("_"): - return - if verbose > 0: - bestrel = get_best_relpath(fixture_def.func) - funcargspec = "{} -- {}".format(argname, bestrel) - else: - funcargspec = argname - tw.line(funcargspec, green=True) - fixture_doc = fixture_def.func.__doc__ - if fixture_doc: - write_docstring(tw, fixture_doc) - else: - tw.line(" no docstring available", red=True) - - def write_item(item): - try: - info = item._fixtureinfo - except AttributeError: - # doctests items have no _fixtureinfo attribute - return - if not info.name2fixturedefs: - # this test item does not use any fixtures - return - tw.line() - tw.sep("-", "fixtures used by {}".format(item.name)) - tw.sep("-", "({})".format(get_best_relpath(item.function))) - # 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 - # 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) - - -def showfixtures(config): - from _pytest.main import wrap_session - - return wrap_session(config, _showfixtures_main) - - -def _showfixtures_main(config, session): - 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() - - for argname, fixturedefs in fm._arg2fixturedefs.items(): - assert fixturedefs is not None - if not fixturedefs: - continue - for fixturedef in fixturedefs: - loc = getlocation(fixturedef.func, curdir) - if (fixturedef.argname, loc) in seen: - continue - seen.add((fixturedef.argname, loc)) - available.append( - ( - len(fixturedef.baseid), - fixturedef.func.__module__, - curdir.bestrelpath(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() - tw.sep("-", "fixtures defined from %s" % (module,)) - currentmodule = module - if verbose <= 0 and argname[0] == "_": - continue + + +def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None): + ids = [ + _idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item) + for valindex, parameterset in enumerate(parametersets) + ] + if len(set(ids)) != len(ids): + # The ids are not unique + duplicates = [testid for testid in ids if ids.count(testid) > 1] + counters = collections.defaultdict(lambda: 0) + for index, testid in enumerate(ids): + if testid in duplicates: + ids[index] = testid + str(counters[testid]) + counters[testid] += 1 + return 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, session): + 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, curdir) + return curdir.bestrelpath(loc) + + def write_fixture(fixture_def): + argname = fixture_def.argname + if verbose <= 0 and argname.startswith("_"): + return + if verbose > 0: + bestrel = get_best_relpath(fixture_def.func) + funcargspec = "{} -- {}".format(argname, bestrel) + else: + funcargspec = argname + tw.line(funcargspec, green=True) + fixture_doc = fixture_def.func.__doc__ + if fixture_doc: + write_docstring(tw, fixture_doc) + else: + tw.line(" no docstring available", red=True) + + def write_item(item): + try: + info = item._fixtureinfo + except AttributeError: + # doctests items have no _fixtureinfo attribute + return + if not info.name2fixturedefs: + # this test item does not use any fixtures + return + tw.line() + tw.sep("-", "fixtures used by {}".format(item.name)) + tw.sep("-", "({})".format(get_best_relpath(item.function))) + # 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 + # 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) + + +def showfixtures(config): + from _pytest.main import wrap_session + + return wrap_session(config, _showfixtures_main) + + +def _showfixtures_main(config, session): + 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() + + for argname, fixturedefs in fm._arg2fixturedefs.items(): + assert fixturedefs is not None + if not fixturedefs: + continue + for fixturedef in fixturedefs: + loc = getlocation(fixturedef.func, curdir) + if (fixturedef.argname, loc) in seen: + continue + seen.add((fixturedef.argname, loc)) + available.append( + ( + len(fixturedef.baseid), + fixturedef.func.__module__, + curdir.bestrelpath(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() + tw.sep("-", "fixtures defined from %s" % (module,)) + 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, curdir) - doc = fixturedef.func.__doc__ or "" - if doc: - write_docstring(tw, doc) - else: - tw.line(" %s: no docstring available" % (loc,), red=True) + loc = getlocation(fixturedef.func, curdir) + doc = fixturedef.func.__doc__ or "" + if doc: + write_docstring(tw, doc) + else: + tw.line(" %s: no docstring available" % (loc,), red=True) tw.line() - - + + def write_docstring(tw, doc, indent=" "): - doc = doc.rstrip() - if "\n" in doc: - firstline, rest = doc.split("\n", 1) - else: - firstline, rest = doc, "" - - if firstline.strip(): + doc = doc.rstrip() + if "\n" in doc: + firstline, rest = doc.split("\n", 1) + else: + firstline, rest = doc, "" + + if firstline.strip(): tw.line(indent + firstline.strip()) - - if rest: - for line in dedent(rest).split("\n"): + + if rest: + for line in dedent(rest).split("\n"): tw.write(indent + line + "\n") - - -class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): - """ a Function Item is responsible for setting up and executing a - Python test function. - """ - - # disable since functions handle it themselves - _ALLOW_MARKERS = False - - def __init__( - self, - name, - parent, - args=None, - config=None, - callspec=None, - callobj=NOTSET, - keywords=None, - session=None, - fixtureinfo=None, - originalname=None, - ): - super(Function, self).__init__(name, parent, config=config, session=session) - self._args = args - if callobj is not NOTSET: - self.obj = callobj - - 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) - + + +class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): + """ a Function Item is responsible for setting up and executing a + Python test function. + """ + + # disable since functions handle it themselves + _ALLOW_MARKERS = False + + def __init__( + self, + name, + parent, + args=None, + config=None, + callspec=None, + callobj=NOTSET, + keywords=None, + session=None, + fixtureinfo=None, + originalname=None, + ): + super(Function, self).__init__(name, parent, config=config, session=session) + self._args = args + if callobj is not NOTSET: + self.obj = callobj + + 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 @@ -1451,57 +1451,57 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): ) ) - 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 = fixtureinfo - self.fixturenames = fixtureinfo.names_closure - self._initrequest() - - #: original function name, without any decorations (for example - #: parametrization adds a ``"[...]"`` suffix to function names). - #: - #: .. versionadded:: 3.0 - self.originalname = originalname - - def _initrequest(self): - self.funcargs = {} - self._request = fixtures.FixtureRequest(self) - - @property - def function(self): - "underlying python 'function' object" - return getimfunc(self.obj) - - def _getobj(self): - name = self.name - i = name.find("[") # parametrization - if i != -1: - name = name[:i] - return getattr(self.parent.obj, name) - - @property - def _pyfuncitem(self): - "(compatonly) for code expecting pytest-2.2 style request objects" - return self - - def runtest(self): - """ execute the underlying test function. """ - self.ihook.pytest_pyfunc_call(pyfuncitem=self) - - def setup(self): - super(Function, self).setup() - fixtures.fillfixtures(self) - - -class FunctionDefinition(Function): - """ - internal hack until we get actual definition nodes instead of the - crappy metafunc hack - """ - - def runtest(self): - raise RuntimeError("function definitions are not supposed to be used") - - setup = runtest + ) + self._fixtureinfo = fixtureinfo + self.fixturenames = fixtureinfo.names_closure + self._initrequest() + + #: original function name, without any decorations (for example + #: parametrization adds a ``"[...]"`` suffix to function names). + #: + #: .. versionadded:: 3.0 + self.originalname = originalname + + def _initrequest(self): + self.funcargs = {} + self._request = fixtures.FixtureRequest(self) + + @property + def function(self): + "underlying python 'function' object" + return getimfunc(self.obj) + + def _getobj(self): + name = self.name + i = name.find("[") # parametrization + if i != -1: + name = name[:i] + return getattr(self.parent.obj, name) + + @property + def _pyfuncitem(self): + "(compatonly) for code expecting pytest-2.2 style request objects" + return self + + def runtest(self): + """ execute the underlying test function. """ + self.ihook.pytest_pyfunc_call(pyfuncitem=self) + + def setup(self): + super(Function, self).setup() + fixtures.fillfixtures(self) + + +class FunctionDefinition(Function): + """ + internal hack until we get actual definition nodes instead of the + crappy metafunc hack + """ + + def runtest(self): + raise RuntimeError("function definitions are not supposed to be used") + + setup = runtest diff --git a/contrib/python/pytest/py2/_pytest/python_api.py b/contrib/python/pytest/py2/_pytest/python_api.py index 66253dad15..f6e475c3a2 100644 --- a/contrib/python/pytest/py2/_pytest/python_api.py +++ b/contrib/python/pytest/py2/_pytest/python_api.py @@ -1,591 +1,591 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -import math -import pprint -import sys +import math +import pprint +import sys import warnings -from decimal import Decimal -from numbers import Number - -from more_itertools.more import always_iterable -from six.moves import filterfalse -from six.moves import zip - -import _pytest._code +from decimal import Decimal +from numbers import Number + +from more_itertools.more import always_iterable +from six.moves import filterfalse +from six.moves import zip + +import _pytest._code from _pytest import deprecated -from _pytest.compat import isclass +from _pytest.compat import isclass from _pytest.compat import Iterable -from _pytest.compat import Mapping +from _pytest.compat import Mapping from _pytest.compat import Sized -from _pytest.compat import STRING_TYPES -from _pytest.outcomes import fail - -BASE_TYPE = (type, STRING_TYPES) - - -def _cmp_raises_type_error(self, other): - """__cmp__ implementation which raises TypeError. Used - by Approx base classes to implement only == and != and raise a - TypeError for other comparisons. - - Needed in Python 2 only, Python 3 all it takes is not implementing the - other operators at all. - """ - __tracebackhide__ = True - raise TypeError( - "Comparison operators other than == and != not supported by approx objects" - ) - - -def _non_numeric_type_error(value, at): - at_str = " at {}".format(at) if at else "" - return TypeError( - "cannot make approximate comparisons to non-numeric values: {!r} {}".format( - value, at_str - ) - ) - - -# builtin pytest.approx helper - - -class ApproxBase(object): - """ - 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 - - def __init__(self, expected, rel=None, abs=None, nan_ok=False): - __tracebackhide__ = True - self.expected = expected - self.abs = abs - self.rel = rel - self.nan_ok = nan_ok - self._check_type() - - def __repr__(self): - raise NotImplementedError - - def __eq__(self, actual): - return all( - a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) - ) - - __hash__ = None - - def __ne__(self, actual): - return not (actual == self) - - if sys.version_info[0] == 2: - __cmp__ = _cmp_raises_type_error - - def _approx_scalar(self, x): - 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 - - def _check_type(self): - """ - 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): - """ - Perform approximate comparisons where the expected value is numpy array. - """ - - def __repr__(self): - list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) - return "approx({!r})".format(list_scalars) - - if sys.version_info[0] == 2: - __cmp__ = _cmp_raises_type_error - - def __eq__(self, actual): - import numpy as np - - # self.expected is supposed to always be an array here - - if not np.isscalar(actual): - try: - actual = np.asarray(actual) - except: # noqa - raise TypeError("cannot compare '{}' to numpy.ndarray".format(actual)) - - 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): +from _pytest.compat import STRING_TYPES +from _pytest.outcomes import fail + +BASE_TYPE = (type, STRING_TYPES) + + +def _cmp_raises_type_error(self, other): + """__cmp__ implementation which raises TypeError. Used + by Approx base classes to implement only == and != and raise a + TypeError for other comparisons. + + Needed in Python 2 only, Python 3 all it takes is not implementing the + other operators at all. + """ + __tracebackhide__ = True + raise TypeError( + "Comparison operators other than == and != not supported by approx objects" + ) + + +def _non_numeric_type_error(value, at): + at_str = " at {}".format(at) if at else "" + return TypeError( + "cannot make approximate comparisons to non-numeric values: {!r} {}".format( + value, at_str + ) + ) + + +# builtin pytest.approx helper + + +class ApproxBase(object): + """ + 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 + + def __init__(self, expected, rel=None, abs=None, nan_ok=False): + __tracebackhide__ = True + self.expected = expected + self.abs = abs + self.rel = rel + self.nan_ok = nan_ok + self._check_type() + + def __repr__(self): + raise NotImplementedError + + def __eq__(self, actual): + return all( + a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) + ) + + __hash__ = None + + def __ne__(self, actual): + return not (actual == self) + + if sys.version_info[0] == 2: + __cmp__ = _cmp_raises_type_error + + def _approx_scalar(self, x): + 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 + + def _check_type(self): + """ + 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): + """ + Perform approximate comparisons where the expected value is numpy array. + """ + + def __repr__(self): + list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) + return "approx({!r})".format(list_scalars) + + if sys.version_info[0] == 2: + __cmp__ = _cmp_raises_type_error + + def __eq__(self, actual): + import numpy as np + + # self.expected is supposed to always be an array here + + if not np.isscalar(actual): + try: + actual = np.asarray(actual) + except: # noqa + raise TypeError("cannot compare '{}' to numpy.ndarray".format(actual)) + + 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): - """ - Perform approximate comparisons where the expected value is a mapping with - numeric values (the keys can be anything). - """ - - def __repr__(self): - return "approx({!r})".format( - {k: self._approx_scalar(v) for k, v in self.expected.items()} - ) - - def __eq__(self, actual): - if set(actual.keys()) != set(self.expected.keys()): - 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): - __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))) - elif not isinstance(value, Number): - raise _non_numeric_type_error(self.expected, at="key={!r}".format(key)) - - + + +class ApproxMapping(ApproxBase): + """ + Perform approximate comparisons where the expected value is a mapping with + numeric values (the keys can be anything). + """ + + def __repr__(self): + return "approx({!r})".format( + {k: self._approx_scalar(v) for k, v in self.expected.items()} + ) + + def __eq__(self, actual): + if set(actual.keys()) != set(self.expected.keys()): + 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): + __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))) + elif not isinstance(value, Number): + raise _non_numeric_type_error(self.expected, at="key={!r}".format(key)) + + class ApproxSequencelike(ApproxBase): - """ - Perform approximate comparisons where the expected value is a sequence of - numbers. - """ - - def __repr__(self): - 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): - if len(actual) != len(self.expected): - return False - return ApproxBase.__eq__(self, actual) - - def _yield_comparisons(self, actual): - return zip(actual, self.expected) - - def _check_type(self): - __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))) - elif not isinstance(x, Number): - raise _non_numeric_type_error( - self.expected, at="index {}".format(index) - ) - - -class ApproxScalar(ApproxBase): - """ - Perform approximate comparisons where the expected value is a single number. - """ - - DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 - DEFAULT_RELATIVE_TOLERANCE = 1e-6 - - def __repr__(self): - """ - Return a string communicating both the expected value and the tolerance - for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode - plus/minus symbol if this is python3 (it's too hard to get right for - python2). - """ - if isinstance(self.expected, complex): - return str(self.expected) - - # Infinities aren't compared using tolerances, so don't show a - # tolerance. - if math.isinf(self.expected): - 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 = "{:.1e}".format(self.tolerance) - except ValueError: - vetted_tolerance = "???" - - if sys.version_info[0] == 2: - return "{} +- {}".format(self.expected, vetted_tolerance) - else: - return u"{} \u00b1 {}".format(self.expected, vetted_tolerance) - - def __eq__(self, actual): - """ - Return true if the given value is equal to the expected value within - the pre-specified tolerance. - """ - if _is_numpy_array(actual): - # Call ``__eq__()`` manually to prevent infinite-recursion with - # numpy<1.13. See #3748. - return all(self.__eq__(a) for a in actual.flat) - - # Short-circuit exact equality. - if actual == self.expected: - return True - - # 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)): - return self.nan_ok and math.isnan(abs(actual)) - - # 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)): - return False - - # Return true if the two numbers are within the tolerance. - return abs(self.expected - actual) <= self.tolerance - - __hash__ = None - - @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( - "absolute tolerance can't be negative: {}".format(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( - "relative tolerance can't be negative: {}".format(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): - """ - Perform approximate comparisons where the expected value is a decimal. - """ - - DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") - DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") - - -def approx(expected, rel=None, abs=None, nan_ok=False): - """ - 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 - - 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. Only available in python>=3.5. `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...`__ - - __ http://docs.scipy.org/doc/numpy-1.10.0/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...`__ - + """ + Perform approximate comparisons where the expected value is a sequence of + numbers. + """ + + def __repr__(self): + 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): + if len(actual) != len(self.expected): + return False + return ApproxBase.__eq__(self, actual) + + def _yield_comparisons(self, actual): + return zip(actual, self.expected) + + def _check_type(self): + __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))) + elif not isinstance(x, Number): + raise _non_numeric_type_error( + self.expected, at="index {}".format(index) + ) + + +class ApproxScalar(ApproxBase): + """ + Perform approximate comparisons where the expected value is a single number. + """ + + DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 + DEFAULT_RELATIVE_TOLERANCE = 1e-6 + + def __repr__(self): + """ + Return a string communicating both the expected value and the tolerance + for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode + plus/minus symbol if this is python3 (it's too hard to get right for + python2). + """ + if isinstance(self.expected, complex): + return str(self.expected) + + # Infinities aren't compared using tolerances, so don't show a + # tolerance. + if math.isinf(self.expected): + 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 = "{:.1e}".format(self.tolerance) + except ValueError: + vetted_tolerance = "???" + + if sys.version_info[0] == 2: + return "{} +- {}".format(self.expected, vetted_tolerance) + else: + return u"{} \u00b1 {}".format(self.expected, vetted_tolerance) + + def __eq__(self, actual): + """ + Return true if the given value is equal to the expected value within + the pre-specified tolerance. + """ + if _is_numpy_array(actual): + # Call ``__eq__()`` manually to prevent infinite-recursion with + # numpy<1.13. See #3748. + return all(self.__eq__(a) for a in actual.flat) + + # Short-circuit exact equality. + if actual == self.expected: + return True + + # 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)): + return self.nan_ok and math.isnan(abs(actual)) + + # 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)): + return False + + # Return true if the two numbers are within the tolerance. + return abs(self.expected - actual) <= self.tolerance + + __hash__ = None + + @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( + "absolute tolerance can't be negative: {}".format(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( + "relative tolerance can't be negative: {}".format(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): + """ + Perform approximate comparisons where the expected value is a decimal. + """ + + DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12") + DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6") + + +def approx(expected, rel=None, abs=None, nan_ok=False): + """ + 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 + + 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. Only available in python>=3.5. `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...`__ + + __ http://docs.scipy.org/doc/numpy-1.10.0/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...`__ + __ 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__ - """ - - # 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 = ApproxDecimal - elif isinstance(expected, Number): - cls = ApproxScalar - elif isinstance(expected, Mapping): - cls = ApproxMapping - elif _is_numpy_array(expected): - cls = ApproxNumpy + + - ``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__ + """ + + # 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 = ApproxDecimal + elif isinstance(expected, Number): + cls = ApproxScalar + elif isinstance(expected, Mapping): + cls = ApproxMapping + elif _is_numpy_array(expected): + cls = ApproxNumpy elif ( isinstance(expected, Iterable) and isinstance(expected, Sized) and not isinstance(expected, STRING_TYPES) ): cls = ApproxSequencelike - else: - raise _non_numeric_type_error(expected, at=None) - - return cls(expected, rel, abs, nan_ok) - - -def _is_numpy_array(obj): - """ - Return true if the given object is a numpy array. Make a special effort to - avoid importing numpy unless it's really necessary. - """ - import sys - - np = sys.modules.get("numpy") - if np is not None: - return isinstance(obj, np.ndarray) - return False - - -# builtin pytest.raises helper - - -def raises(expected_exception, *args, **kwargs): - r""" - Assert that a code block/function call raises ``expected_exception`` + else: + raise _non_numeric_type_error(expected, at=None) + + return cls(expected, rel, abs, nan_ok) + + +def _is_numpy_array(obj): + """ + Return true if the given object is a numpy array. Make a special effort to + avoid importing numpy unless it's really necessary. + """ + import sys + + np = sys.modules.get("numpy") + if np is not None: + return isinstance(obj, np.ndarray) + return False + + +# builtin pytest.raises helper + + +def raises(expected_exception, *args, **kwargs): + 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``. - + __ https://docs.python.org/3/library/re.html#regular-expression-syntax - + :kwparam message: **(deprecated since 4.1)** if specified, provides a custom failure message if the exception is not raised. See :ref:`the deprecation docs <raises message deprecated>` for a workaround. - + .. currentmodule:: _pytest._code Use ``pytest.raises`` as a context manager, which will capture the exception of the given type:: - >>> with raises(ZeroDivisionError): - ... 1/0 - + >>> with raises(ZeroDivisionError): + ... 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 raises(ValueError, match='must be 0 or None'): ... raise ValueError("value must be 0 or None") - + >>> with raises(ValueError, match=r'must be \d+$'): ... raise ValueError("value must be 42") @@ -605,139 +605,139 @@ def raises(expected_exception, *args, **kwargs): is considered error prone as users often mean to use ``match`` instead. See :ref:`the deprecation docs <raises message deprecated>` for a workaround. - .. 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 raises(ValueError) as exc_info: - ... if value > 10: - ... raise ValueError("value must be <= 10") + .. 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 raises(ValueError) as exc_info: + ... 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):: - - >>> with raises(ValueError) as exc_info: - ... if value > 10: - ... raise ValueError("value must be <= 10") - ... + + Instead, the following approach must be taken (note the difference in + scope):: + + >>> with raises(ValueError) as exc_info: + ... 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 - frame) alive until the next cyclic garbage collection run. See the - official Python ``try`` statement documentation for more detailed - information. - - """ - __tracebackhide__ = True - for exc in filterfalse(isclass, always_iterable(expected_exception, BASE_TYPE)): - msg = ( - "exceptions must be old-style classes or" - " derived from BaseException, not %s" - ) - raise TypeError(msg % type(exc)) - - message = "DID NOT RAISE {}".format(expected_exception) - match_expr = None - - if not args: - if "message" in kwargs: - message = kwargs.pop("message") + + .. 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. See the + official Python ``try`` statement documentation for more detailed + information. + + """ + __tracebackhide__ = True + for exc in filterfalse(isclass, always_iterable(expected_exception, BASE_TYPE)): + msg = ( + "exceptions must be old-style classes or" + " derived from BaseException, not %s" + ) + raise TypeError(msg % type(exc)) + + message = "DID NOT RAISE {}".format(expected_exception) + match_expr = None + + if not args: + if "message" in kwargs: + message = kwargs.pop("message") warnings.warn(deprecated.RAISES_MESSAGE_PARAMETER, stacklevel=2) - if "match" in kwargs: - match_expr = kwargs.pop("match") - if kwargs: - msg = "Unexpected keyword arguments passed to pytest.raises: " + if "match" in kwargs: + match_expr = kwargs.pop("match") + if kwargs: + msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) - raise TypeError(msg) - return RaisesContext(expected_exception, message, match_expr) - elif isinstance(args[0], str): + raise TypeError(msg) + return RaisesContext(expected_exception, message, match_expr) + elif isinstance(args[0], str): warnings.warn(deprecated.RAISES_EXEC, stacklevel=2) (code,) = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - # print "raises frame scope: %r" % frame.f_locals - try: + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + # print "raises frame scope: %r" % frame.f_locals + try: code = _pytest._code.Source(code).compile(_genframe=frame) exec(code, frame.f_globals, loc) - # XXX didn't mean f_globals == f_locals something special? - # this is destroyed here ... - except expected_exception: + # XXX didn't mean f_globals == f_locals something special? + # this is destroyed here ... + except expected_exception: return _pytest._code.ExceptionInfo.from_current() - else: - func = args[0] - try: - func(*args[1:], **kwargs) - except expected_exception: + else: + func = args[0] + try: + func(*args[1:], **kwargs) + except expected_exception: return _pytest._code.ExceptionInfo.from_current() - fail(message) - - -raises.Exception = fail.Exception - - -class RaisesContext(object): - def __init__(self, expected_exception, message, match_expr): - self.expected_exception = expected_exception - self.message = message - self.match_expr = match_expr - self.excinfo = None - - def __enter__(self): + fail(message) + + +raises.Exception = fail.Exception + + +class RaisesContext(object): + def __init__(self, expected_exception, message, match_expr): + self.expected_exception = expected_exception + self.message = message + self.match_expr = match_expr + self.excinfo = None + + def __enter__(self): self.excinfo = _pytest._code.ExceptionInfo.for_later() - return self.excinfo - - def __exit__(self, *tp): - __tracebackhide__ = True - if tp[0] is None: - fail(self.message) - self.excinfo.__init__(tp) - suppress_exception = issubclass(self.excinfo.type, self.expected_exception) - if sys.version_info[0] == 2 and suppress_exception: - sys.exc_clear() + return self.excinfo + + def __exit__(self, *tp): + __tracebackhide__ = True + if tp[0] is None: + fail(self.message) + self.excinfo.__init__(tp) + suppress_exception = issubclass(self.excinfo.type, self.expected_exception) + if sys.version_info[0] == 2 and suppress_exception: + sys.exc_clear() if self.match_expr is not None and suppress_exception: - self.excinfo.match(self.match_expr) - return suppress_exception + self.excinfo.match(self.match_expr) + return suppress_exception diff --git a/contrib/python/pytest/py2/_pytest/recwarn.py b/contrib/python/pytest/py2/_pytest/recwarn.py index 8f7e999eef..7abf2e9355 100644 --- a/contrib/python/pytest/py2/_pytest/recwarn.py +++ b/contrib/python/pytest/py2/_pytest/recwarn.py @@ -1,251 +1,251 @@ # -*- coding: utf-8 -*- -""" recording warnings during test function execution. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import inspect -import re -import sys -import warnings - -import six - -import _pytest._code +""" recording warnings during test function execution. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import inspect +import re +import sys +import warnings + +import six + +import _pytest._code from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS from _pytest.deprecated import WARNS_EXEC -from _pytest.fixtures import yield_fixture -from _pytest.outcomes import fail - - -@yield_fixture -def recwarn(): - """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() - with wrec: - warnings.simplefilter("default") - yield wrec - - -def deprecated_call(func=None, *args, **kwargs): - """context manager that can be used to ensure a block of code triggers a - ``DeprecationWarning`` or ``PendingDeprecationWarning``:: - - >>> import warnings - >>> def api_call_v2(): - ... warnings.warn('use v3 of this api', DeprecationWarning) - ... return 200 - - >>> with deprecated_call(): - ... assert api_call_v2() == 200 - - ``deprecated_call`` 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. - """ - __tracebackhide__ = True - if func is not None: - args = (func,) + args - return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) - - -def warns(expected_warning, *args, **kwargs): - 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 - ``pytest.raises`` can be used:: - - >>> with warns(RuntimeWarning): - ... warnings.warn("my warning", RuntimeWarning) - - In the context manager form you may use the keyword argument ``match`` to assert - that the exception matches a text or regex:: - - >>> with warns(UserWarning, match='must be 0 or None'): - ... warnings.warn("value must be 0 or None", UserWarning) - - >>> with warns(UserWarning, match=r'must be \d+$'): - ... warnings.warn("value must be 42", UserWarning) - - >>> with 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: +from _pytest.fixtures import yield_fixture +from _pytest.outcomes import fail + + +@yield_fixture +def recwarn(): + """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() + with wrec: + warnings.simplefilter("default") + yield wrec + + +def deprecated_call(func=None, *args, **kwargs): + """context manager that can be used to ensure a block of code triggers a + ``DeprecationWarning`` or ``PendingDeprecationWarning``:: + + >>> import warnings + >>> def api_call_v2(): + ... warnings.warn('use v3 of this api', DeprecationWarning) + ... return 200 + + >>> with deprecated_call(): + ... assert api_call_v2() == 200 + + ``deprecated_call`` 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. + """ + __tracebackhide__ = True + if func is not None: + args = (func,) + args + return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) + + +def warns(expected_warning, *args, **kwargs): + 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 + ``pytest.raises`` can be used:: + + >>> with warns(RuntimeWarning): + ... warnings.warn("my warning", RuntimeWarning) + + In the context manager form you may use the keyword argument ``match`` to assert + that the exception matches a text or regex:: + + >>> with warns(UserWarning, match='must be 0 or None'): + ... warnings.warn("value must be 0 or None", UserWarning) + + >>> with warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("value must be 42", UserWarning) + + >>> with 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: match_expr = kwargs.pop("match", None) if kwargs: warnings.warn( PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2 ) - return WarningsChecker(expected_warning, match_expr=match_expr) - elif isinstance(args[0], str): + return WarningsChecker(expected_warning, match_expr=match_expr) + elif isinstance(args[0], str): warnings.warn(WARNS_EXEC, stacklevel=2) (code,) = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + with WarningsChecker(expected_warning): - code = _pytest._code.Source(code).compile() + code = _pytest._code.Source(code).compile() exec(code, frame.f_globals, loc) - else: - func = args[0] + else: + func = args[0] with WarningsChecker(expected_warning): - 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): - super(WarningsRecorder, self).__init__(record=True) - self._entered = False - self._list = [] - - @property - def list(self): - """The list of recorded warnings.""" - return self._list - - def __getitem__(self, i): - """Get a recorded warning by index.""" - return self._list[i] - - def __iter__(self): - """Iterate through the recorded warnings.""" - return iter(self._list) - - def __len__(self): - """The number of recorded warnings.""" - return len(self._list) - - def pop(self, cls=Warning): - """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): - """Clear the list of recorded warnings.""" - self._list[:] = [] - - def __enter__(self): - if self._entered: - __tracebackhide__ = True - raise RuntimeError("Cannot enter %r twice" % self) - self._list = super(WarningsRecorder, self).__enter__() - warnings.simplefilter("always") - # python3 keeps track of a "filter version", when the filters are - # updated previously seen warnings can be re-warned. python2 has no - # concept of this so we must reset the warnings registry manually. - # trivial patching of `warnings.warn` seems to be enough somehow? - if six.PY2: - - def warn(message, category=None, stacklevel=1): - # duplicate the stdlib logic due to - # bad handing in the c version of warnings - if isinstance(message, Warning): - category = message.__class__ - # Check category argument - if category is None: - category = UserWarning - assert issubclass(category, Warning) - - # emulate resetting the warn registry - f_globals = sys._getframe(stacklevel).f_globals - if "__warningregistry__" in f_globals: - orig = f_globals["__warningregistry__"] - f_globals["__warningregistry__"] = None - try: - return self._saved_warn(message, category, stacklevel + 1) - finally: - f_globals["__warningregistry__"] = orig - else: - return self._saved_warn(message, category, stacklevel + 1) - - warnings.warn, self._saved_warn = warn, warnings.warn - return self - - def __exit__(self, *exc_info): - if not self._entered: - __tracebackhide__ = True - raise RuntimeError("Cannot exit %r without entering first" % self) - # see above where `self._saved_warn` is assigned - if six.PY2: - warnings.warn = self._saved_warn - super(WarningsRecorder, self).__exit__(*exc_info) - + 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): + super(WarningsRecorder, self).__init__(record=True) + self._entered = False + self._list = [] + + @property + def list(self): + """The list of recorded warnings.""" + return self._list + + def __getitem__(self, i): + """Get a recorded warning by index.""" + return self._list[i] + + def __iter__(self): + """Iterate through the recorded warnings.""" + return iter(self._list) + + def __len__(self): + """The number of recorded warnings.""" + return len(self._list) + + def pop(self, cls=Warning): + """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): + """Clear the list of recorded warnings.""" + self._list[:] = [] + + def __enter__(self): + if self._entered: + __tracebackhide__ = True + raise RuntimeError("Cannot enter %r twice" % self) + self._list = super(WarningsRecorder, self).__enter__() + warnings.simplefilter("always") + # python3 keeps track of a "filter version", when the filters are + # updated previously seen warnings can be re-warned. python2 has no + # concept of this so we must reset the warnings registry manually. + # trivial patching of `warnings.warn` seems to be enough somehow? + if six.PY2: + + def warn(message, category=None, stacklevel=1): + # duplicate the stdlib logic due to + # bad handing in the c version of warnings + if isinstance(message, Warning): + category = message.__class__ + # Check category argument + if category is None: + category = UserWarning + assert issubclass(category, Warning) + + # emulate resetting the warn registry + f_globals = sys._getframe(stacklevel).f_globals + if "__warningregistry__" in f_globals: + orig = f_globals["__warningregistry__"] + f_globals["__warningregistry__"] = None + try: + return self._saved_warn(message, category, stacklevel + 1) + finally: + f_globals["__warningregistry__"] = orig + else: + return self._saved_warn(message, category, stacklevel + 1) + + warnings.warn, self._saved_warn = warn, warnings.warn + return self + + def __exit__(self, *exc_info): + if not self._entered: + __tracebackhide__ = True + raise RuntimeError("Cannot exit %r without entering first" % self) + # see above where `self._saved_warn` is assigned + if six.PY2: + warnings.warn = self._saved_warn + super(WarningsRecorder, self).__exit__(*exc_info) + # 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 - - -class WarningsChecker(WarningsRecorder): - def __init__(self, expected_warning=None, match_expr=None): - super(WarningsChecker, self).__init__() - - msg = "exceptions must be old-style classes or derived from Warning, not %s" - if isinstance(expected_warning, tuple): - for exc in expected_warning: - if not inspect.isclass(exc): - raise TypeError(msg % type(exc)) - elif inspect.isclass(expected_warning): - expected_warning = (expected_warning,) - elif expected_warning is not None: - raise TypeError(msg % type(expected_warning)) - - self.expected_warning = expected_warning - self.match_expr = match_expr - - def __exit__(self, *exc_info): - super(WarningsChecker, self).__exit__(*exc_info) - - __tracebackhide__ = True - - # only check if we're not currently handling an exception - if all(a is None for a in exc_info): - 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], - ) - ) + + +class WarningsChecker(WarningsRecorder): + def __init__(self, expected_warning=None, match_expr=None): + super(WarningsChecker, self).__init__() + + msg = "exceptions must be old-style classes or derived from Warning, not %s" + if isinstance(expected_warning, tuple): + for exc in expected_warning: + if not inspect.isclass(exc): + raise TypeError(msg % type(exc)) + elif inspect.isclass(expected_warning): + expected_warning = (expected_warning,) + elif expected_warning is not None: + raise TypeError(msg % type(expected_warning)) + + self.expected_warning = expected_warning + self.match_expr = match_expr + + def __exit__(self, *exc_info): + super(WarningsChecker, self).__exit__(*exc_info) + + __tracebackhide__ = True + + # only check if we're not currently handling an exception + if all(a is None for a in exc_info): + 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/py2/_pytest/reports.py b/contrib/python/pytest/py2/_pytest/reports.py index 8e5106e069..0bba6762c3 100644 --- a/contrib/python/pytest/py2/_pytest/reports.py +++ b/contrib/python/pytest/py2/_pytest/reports.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from pprint import pprint -import py +import py import six - + from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprEntry from _pytest._code.code import ReprEntryNative @@ -12,111 +12,111 @@ 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.outcomes import skip from _pytest.pathlib import Path - - -def getslaveinfoline(node): - try: - return node._slaveinfocache - except AttributeError: - d = node.slaveinfo - ver = "%s.%s.%s" % d["version_info"][:3] - node._slaveinfocache = s = "[%s] %s -- Python %s %s" % ( - d["id"], - d["sysplatform"], - ver, - d["executable"], - ) - return s - - -class BaseReport(object): + + +def getslaveinfoline(node): + try: + return node._slaveinfocache + except AttributeError: + d = node.slaveinfo + ver = "%s.%s.%s" % d["version_info"][:3] + node._slaveinfocache = s = "[%s] %s -- Python %s %s" % ( + d["id"], + d["sysplatform"], + ver, + d["executable"], + ) + return s + + +class BaseReport(object): when = None location = None - def __init__(self, **kw): - self.__dict__.update(kw) - - def toterminal(self, out): - if hasattr(self, "node"): - out.line(getslaveinfoline(self.node)) - - longrepr = self.longrepr - if longrepr is None: - return - - if hasattr(longrepr, "toterminal"): - longrepr.toterminal(out) - else: - try: - out.line(longrepr) - except UnicodeEncodeError: - out.line("<unprintable longrepr>") - - def get_sections(self, prefix): - for name, content in self.sections: - if name.startswith(prefix): - yield prefix, content - - @property - def longreprtext(self): - """ - Read-only property that returns the full string representation - of ``longrepr``. - - .. versionadded:: 3.0 - """ - tw = py.io.TerminalWriter(stringio=True) - tw.hasmarkup = False - self.toterminal(tw) - exc = tw.stringio.getvalue() - return exc.strip() - - @property - def caplog(self): - """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 - def capstdout(self): - """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 - def capstderr(self): - """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 - def fspath(self): - return self.nodeid.split("::")[0] - + def __init__(self, **kw): + self.__dict__.update(kw) + + def toterminal(self, out): + if hasattr(self, "node"): + out.line(getslaveinfoline(self.node)) + + longrepr = self.longrepr + if longrepr is None: + return + + if hasattr(longrepr, "toterminal"): + longrepr.toterminal(out) + else: + try: + out.line(longrepr) + except UnicodeEncodeError: + out.line("<unprintable longrepr>") + + def get_sections(self, prefix): + for name, content in self.sections: + if name.startswith(prefix): + yield prefix, content + + @property + def longreprtext(self): + """ + Read-only property that returns the full string representation + of ``longrepr``. + + .. versionadded:: 3.0 + """ + tw = py.io.TerminalWriter(stringio=True) + tw.hasmarkup = False + self.toterminal(tw) + exc = tw.stringio.getvalue() + return exc.strip() + + @property + def caplog(self): + """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 + def capstdout(self): + """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 + def capstderr(self): + """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 + def fspath(self): + return self.nodeid.split("::")[0] + @property def count_towards_summary(self): """ **Experimental** - + Returns True if this report should be counted towards the totals shown at the end of the test session: "1 passed, 1 failure, etc". @@ -278,70 +278,70 @@ def _report_unserialization_failure(type_name, report_class, reportdict): raise RuntimeError(stream.getvalue()) -class TestReport(BaseReport): - """ Basic test report object (also used for setup and teardown calls if - they fail). - """ - +class TestReport(BaseReport): + """ Basic test report object (also used for setup and teardown calls if + they fail). + """ + __test__ = False - def __init__( - self, - nodeid, - location, - keywords, - outcome, - longrepr, - when, - sections=(), - duration=0, - user_properties=None, - **extra - ): - #: normalized collection node id - 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. - self.location = location - - #: a name -> value dictionary containing all keywords and - #: 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 - - #: one of 'setup', 'call', 'teardown' to indicate runtest phase. - 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 []) - - #: 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) - - #: time it took to run just the test - self.duration = duration - - self.__dict__.update(extra) - - def __repr__(self): + def __init__( + self, + nodeid, + location, + keywords, + outcome, + longrepr, + when, + sections=(), + duration=0, + user_properties=None, + **extra + ): + #: normalized collection node id + 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. + self.location = location + + #: a name -> value dictionary containing all keywords and + #: 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 + + #: one of 'setup', 'call', 'teardown' to indicate runtest phase. + 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 []) + + #: 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) + + #: time it took to run just the test + self.duration = duration + + self.__dict__.update(extra) + + def __repr__(self): return "<%s %r when=%r outcome=%r>" % ( self.__class__.__name__, - self.nodeid, - self.when, - self.outcome, - ) - + self.nodeid, + self.when, + self.outcome, + ) + @classmethod def from_item_and_call(cls, item, call): """ @@ -384,37 +384,37 @@ class TestReport(BaseReport): duration, user_properties=item.user_properties, ) - - + + class CollectReport(BaseReport): when = "collect" - - def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): - self.nodeid = nodeid - self.outcome = outcome - self.longrepr = longrepr - self.result = result or [] - self.sections = list(sections) - self.__dict__.update(extra) - - @property - def location(self): - return (self.fspath, None, self.fspath) - - def __repr__(self): - return "<CollectReport %r lenresult=%s outcome=%r>" % ( - self.nodeid, - len(self.result), - self.outcome, - ) - - -class CollectErrorRepr(TerminalRepr): - def __init__(self, msg): - self.longrepr = msg - - def toterminal(self, out): - out.line(self.longrepr, red=True) + + def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): + self.nodeid = nodeid + self.outcome = outcome + self.longrepr = longrepr + self.result = result or [] + self.sections = list(sections) + self.__dict__.update(extra) + + @property + def location(self): + return (self.fspath, None, self.fspath) + + def __repr__(self): + return "<CollectReport %r lenresult=%s outcome=%r>" % ( + self.nodeid, + len(self.result), + self.outcome, + ) + + +class CollectErrorRepr(TerminalRepr): + def __init__(self, msg): + self.longrepr = msg + + def toterminal(self, out): + out.line(self.longrepr, red=True) def pytest_report_to_serializable(report): diff --git a/contrib/python/pytest/py2/_pytest/resultlog.py b/contrib/python/pytest/py2/_pytest/resultlog.py index aa7e9c9025..bd30b5071e 100644 --- a/contrib/python/pytest/py2/_pytest/resultlog.py +++ b/contrib/python/pytest/py2/_pytest/resultlog.py @@ -1,102 +1,102 @@ # -*- coding: utf-8 -*- -""" log machine-parseable test session result information in a plain -text file. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os - -import py - - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting", "resultlog plugin options") - group.addoption( - "--resultlog", - "--result-log", - action="store", - metavar="path", - default=None, - help="DEPRECATED path for machine-readable result log.", - ) - - -def pytest_configure(config): - resultlog = config.option.resultlog - # prevent opening resultlog on slave nodes (xdist) - if resultlog and not hasattr(config, "slaveinput"): - dirname = os.path.dirname(os.path.abspath(resultlog)) - if not os.path.isdir(dirname): - os.makedirs(dirname) - logfile = open(resultlog, "w", 1) # line buffered - config._resultlog = ResultLog(config, logfile) - config.pluginmanager.register(config._resultlog) - - from _pytest.deprecated import RESULT_LOG +""" log machine-parseable test session result information in a plain +text file. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os + +import py + + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting", "resultlog plugin options") + group.addoption( + "--resultlog", + "--result-log", + action="store", + metavar="path", + default=None, + help="DEPRECATED path for machine-readable result log.", + ) + + +def pytest_configure(config): + resultlog = config.option.resultlog + # prevent opening resultlog on slave nodes (xdist) + if resultlog and not hasattr(config, "slaveinput"): + dirname = os.path.dirname(os.path.abspath(resultlog)) + if not os.path.isdir(dirname): + os.makedirs(dirname) + logfile = open(resultlog, "w", 1) # line buffered + config._resultlog = ResultLog(config, logfile) + config.pluginmanager.register(config._resultlog) + + from _pytest.deprecated import RESULT_LOG from _pytest.warnings import _issue_warning_captured - + _issue_warning_captured(RESULT_LOG, config.hook, stacklevel=2) - - -def pytest_unconfigure(config): - resultlog = getattr(config, "_resultlog", None) - if resultlog: - resultlog.logfile.close() - del config._resultlog - config.pluginmanager.unregister(resultlog) - - -class ResultLog(object): - def __init__(self, config, logfile): - self.config = config - self.logfile = logfile # preferably line buffered - - def write_log_entry(self, testpath, lettercode, longrepr): - print("%s %s" % (lettercode, testpath), file=self.logfile) - for line in longrepr.splitlines(): - print(" %s" % line, file=self.logfile) - - def log_outcome(self, report, lettercode, longrepr): - testpath = getattr(report, "nodeid", None) - if testpath is None: - testpath = report.fspath - self.write_log_entry(testpath, lettercode, longrepr) - - def pytest_runtest_logreport(self, report): - if report.when != "call" and report.passed: - return + + +def pytest_unconfigure(config): + resultlog = getattr(config, "_resultlog", None) + if resultlog: + resultlog.logfile.close() + del config._resultlog + config.pluginmanager.unregister(resultlog) + + +class ResultLog(object): + def __init__(self, config, logfile): + self.config = config + self.logfile = logfile # preferably line buffered + + def write_log_entry(self, testpath, lettercode, longrepr): + print("%s %s" % (lettercode, testpath), file=self.logfile) + for line in longrepr.splitlines(): + print(" %s" % line, file=self.logfile) + + def log_outcome(self, report, lettercode, longrepr): + testpath = getattr(report, "nodeid", None) + if testpath is None: + testpath = report.fspath + self.write_log_entry(testpath, lettercode, longrepr) + + def pytest_runtest_logreport(self, report): + if report.when != "call" and report.passed: + return res = self.config.hook.pytest_report_teststatus( report=report, config=self.config ) - code = res[1] - if code == "x": - longrepr = str(report.longrepr) - elif code == "X": - longrepr = "" - elif report.passed: - longrepr = "" - elif report.failed: - longrepr = str(report.longrepr) - elif report.skipped: - longrepr = str(report.longrepr[2]) - self.log_outcome(report, code, longrepr) - - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - code = "F" - longrepr = str(report.longrepr) - else: - assert report.skipped - code = "S" - longrepr = "%s:%d: %s" % report.longrepr - self.log_outcome(report, code, longrepr) - - def pytest_internalerror(self, excrepr): - reprcrash = getattr(excrepr, "reprcrash", None) - path = getattr(reprcrash, "path", None) - if path is None: - path = "cwd:%s" % py.path.local() - self.write_log_entry(path, "!", str(excrepr)) + code = res[1] + if code == "x": + longrepr = str(report.longrepr) + elif code == "X": + longrepr = "" + elif report.passed: + longrepr = "" + elif report.failed: + longrepr = str(report.longrepr) + elif report.skipped: + longrepr = str(report.longrepr[2]) + self.log_outcome(report, code, longrepr) + + def pytest_collectreport(self, report): + if not report.passed: + if report.failed: + code = "F" + longrepr = str(report.longrepr) + else: + assert report.skipped + code = "S" + longrepr = "%s:%d: %s" % report.longrepr + self.log_outcome(report, code, longrepr) + + def pytest_internalerror(self, excrepr): + reprcrash = getattr(excrepr, "reprcrash", None) + path = getattr(reprcrash, "path", None) + if path is None: + path = "cwd:%s" % py.path.local() + self.write_log_entry(path, "!", str(excrepr)) diff --git a/contrib/python/pytest/py2/_pytest/runner.py b/contrib/python/pytest/py2/_pytest/runner.py index 154a93983d..34ae917738 100644 --- a/contrib/python/pytest/py2/_pytest/runner.py +++ b/contrib/python/pytest/py2/_pytest/runner.py @@ -1,216 +1,216 @@ # -*- coding: utf-8 -*- -""" basic collect and runtest protocol implementations """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import bdb -import os -import sys -from time import time - +""" basic collect and runtest protocol implementations """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import bdb +import os +import sys +from time import time + import attr -import six - -from .reports import CollectErrorRepr -from .reports import CollectReport -from .reports import TestReport -from _pytest._code.code import ExceptionInfo +import six + +from .reports import CollectErrorRepr +from .reports import CollectReport +from .reports import TestReport +from _pytest._code.code import ExceptionInfo from _pytest.compat import safe_str from _pytest.outcomes import Exit -from _pytest.outcomes import Skipped -from _pytest.outcomes import TEST_OUTCOME - -# -# pytest plugin hooks - - -def pytest_addoption(parser): - 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).", - ), - - -def pytest_terminal_summary(terminalreporter): - durations = terminalreporter.config.option.durations - 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) - dlist.reverse() - if not durations: - tr.write_sep("=", "slowest test durations") - else: - tr.write_sep("=", "slowest %s test durations" % durations) - dlist = dlist[:durations] - - for rep in dlist: - if verbose < 2 and rep.duration < 0.005: - tr.write_line("") - tr.write_line("(0.00 durations hidden. Use -vv to show these durations.)") - break - tr.write_line("%02.2fs %-8s %s" % (rep.duration, rep.when, rep.nodeid)) - - -def pytest_sessionstart(session): - session._setupstate = SetupState() - - -def pytest_sessionfinish(session): - session._setupstate.teardown_all() - - -def pytest_runtest_protocol(item, nextitem): - item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) - runtestprotocol(item, nextitem=nextitem) - item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) - return True - - -def runtestprotocol(item, log=True, nextitem=None): - hasrequest = hasattr(item, "_request") - if hasrequest and not item._request: - item._initrequest() - rep = call_and_report(item, "setup", log) - reports = [rep] - if rep.passed: +from _pytest.outcomes import Skipped +from _pytest.outcomes import TEST_OUTCOME + +# +# pytest plugin hooks + + +def pytest_addoption(parser): + 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).", + ), + + +def pytest_terminal_summary(terminalreporter): + durations = terminalreporter.config.option.durations + 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) + dlist.reverse() + if not durations: + tr.write_sep("=", "slowest test durations") + else: + tr.write_sep("=", "slowest %s test durations" % durations) + dlist = dlist[:durations] + + for rep in dlist: + if verbose < 2 and rep.duration < 0.005: + tr.write_line("") + tr.write_line("(0.00 durations hidden. Use -vv to show these durations.)") + break + tr.write_line("%02.2fs %-8s %s" % (rep.duration, rep.when, rep.nodeid)) + + +def pytest_sessionstart(session): + session._setupstate = SetupState() + + +def pytest_sessionfinish(session): + session._setupstate.teardown_all() + + +def pytest_runtest_protocol(item, nextitem): + item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) + runtestprotocol(item, nextitem=nextitem) + item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) + return True + + +def runtestprotocol(item, log=True, nextitem=None): + hasrequest = hasattr(item, "_request") + if hasrequest and not item._request: + item._initrequest() + 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)) - # after all teardown hooks have been called - # want funcargs and request info to go away - if hasrequest: - item._request = False - item.funcargs = None - return reports - - -def show_test_item(item): - """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(item._fixtureinfo.name2fixturedefs.keys()) - if used_fixtures: - tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) - - -def pytest_runtest_setup(item): - _update_current_test_var(item, "setup") - item.session._setupstate.prepare(item) - - -def pytest_runtest_call(item): - _update_current_test_var(item, "call") - sys.last_type, sys.last_value, sys.last_traceback = (None, None, None) - try: - item.runtest() - except Exception: - # Store trace info to allow postmortem debugging - type, value, tb = sys.exc_info() - tb = tb.tb_next # Skip *this* frame - sys.last_type = type - sys.last_value = value - sys.last_traceback = tb - del type, value, tb # Get rid of these in this frame - raise - - -def pytest_runtest_teardown(item, nextitem): - _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, when): - """ - Update 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: - value = "{} ({})".format(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) - - -def pytest_report_teststatus(report): - 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 "", "", "" - - -# -# Implementation - - -def call_and_report(item, when, log=True, **kwds): - call = call_runtest_hook(item, when, **kwds) - hook = item.ihook - report = 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 - - -def check_interactive_exception(call, report): - return call.excinfo and not ( - hasattr(report, "wasxfail") + 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: + item._request = False + item.funcargs = None + return reports + + +def show_test_item(item): + """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(item._fixtureinfo.name2fixturedefs.keys()) + if used_fixtures: + tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) + + +def pytest_runtest_setup(item): + _update_current_test_var(item, "setup") + item.session._setupstate.prepare(item) + + +def pytest_runtest_call(item): + _update_current_test_var(item, "call") + sys.last_type, sys.last_value, sys.last_traceback = (None, None, None) + try: + item.runtest() + except Exception: + # Store trace info to allow postmortem debugging + type, value, tb = sys.exc_info() + tb = tb.tb_next # Skip *this* frame + sys.last_type = type + sys.last_value = value + sys.last_traceback = tb + del type, value, tb # Get rid of these in this frame + raise + + +def pytest_runtest_teardown(item, nextitem): + _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, when): + """ + Update 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: + value = "{} ({})".format(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) + + +def pytest_report_teststatus(report): + 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 "", "", "" + + +# +# Implementation + + +def call_and_report(item, when, log=True, **kwds): + call = call_runtest_hook(item, when, **kwds) + hook = item.ihook + report = 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 + + +def check_interactive_exception(call, report): + return call.excinfo and not ( + hasattr(report, "wasxfail") or call.excinfo.errisinstance(Skipped) - or call.excinfo.errisinstance(bdb.BdbQuit) - ) - - -def call_runtest_hook(item, when, **kwds): - hookname = "pytest_runtest_" + when - ihook = getattr(item.ihook, hookname) + or call.excinfo.errisinstance(bdb.BdbQuit) + ) + + +def call_runtest_hook(item, when, **kwds): + hookname = "pytest_runtest_" + when + ihook = getattr(item.ihook, hookname) reraise = (Exit,) if not item.config.getoption("usepdb", False): reraise += (KeyboardInterrupt,) return CallInfo.from_call( lambda: ihook(item=item, **kwds), when=when, reraise=reraise - ) - - + ) + + @attr.s(repr=False) -class CallInfo(object): - """ Result/Exception info a function invocation. """ - +class CallInfo(object): + """ Result/Exception info a function invocation. """ + _result = attr.ib() # Optional[ExceptionInfo] excinfo = attr.ib() start = attr.ib() stop = attr.ib() when = attr.ib() - + @property def result(self): if self.excinfo is not None: @@ -219,158 +219,158 @@ class CallInfo(object): @classmethod def from_call(cls, func, when, reraise=None): - #: context of invocation: one of "setup", "call", - #: "teardown", "memocollect" + #: context of invocation: one of "setup", "call", + #: "teardown", "memocollect" start = time() excinfo = None - try: + try: result = func() except: # noqa excinfo = ExceptionInfo.from_current() if reraise is not None and excinfo.errisinstance(reraise): - raise + raise result = None stop = time() return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo) - - def __repr__(self): + + def __repr__(self): if self.excinfo is not None: status = "exception" value = self.excinfo.value - else: + else: # TODO: investigate unification value = repr(self._result) status = "result" return "<CallInfo when={when!r} {status}: {value}>".format( when=self.when, value=safe_str(value), status=status ) - - -def pytest_runtest_makereport(item, call): + + +def pytest_runtest_makereport(item, call): return TestReport.from_item_and_call(item, call) - - -def pytest_make_collect_report(collector): + + +def pytest_make_collect_report(collector): call = CallInfo.from_call(lambda: list(collector.collect()), "collect") - longrepr = None - if not call.excinfo: - outcome = "passed" - else: - from _pytest import nose - - skip_exceptions = (Skipped,) + nose.get_skip_exceptions() - if call.excinfo.errisinstance(skip_exceptions): - outcome = "skipped" - r = collector._repr_failure_py(call.excinfo, "line").reprcrash - longrepr = (str(r.path), r.lineno, r.message) - else: - outcome = "failed" - errorinfo = collector.repr_failure(call.excinfo) - if not hasattr(errorinfo, "toterminal"): - errorinfo = CollectErrorRepr(errorinfo) - longrepr = errorinfo - rep = CollectReport( - collector.nodeid, outcome, longrepr, getattr(call, "result", None) - ) - rep.call = call # see collect_one_node - return rep - - -class SetupState(object): - """ shared state for setting up/tearing down test items or collectors. """ - - def __init__(self): - self.stack = [] - self._finalizers = {} - - def addfinalizer(self, finalizer, colitem): - """ attach a finalizer to the given colitem. - if colitem is None, this will add a finalizer that - is called at the end of teardown_all(). - """ - 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): - finalizers = self._finalizers.pop(colitem, None) - exc = None - while finalizers: - fin = finalizers.pop() - try: - fin() - except TEST_OUTCOME: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = sys.exc_info() - if exc: - six.reraise(*exc) - - def _teardown_with_finalization(self, colitem): - self._callfinalizers(colitem) - if hasattr(colitem, "teardown"): - colitem.teardown() - for colitem in self._finalizers: - assert ( - colitem is None or colitem in self.stack or isinstance(colitem, tuple) - ) - - def teardown_all(self): - 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): - needed_collectors = nextitem and nextitem.listchain() or [] - self._teardown_towards(needed_collectors) - - def _teardown_towards(self, needed_collectors): - exc = None - while self.stack: - if self.stack == needed_collectors[: len(self.stack)]: - break - try: - self._pop_and_teardown() - except TEST_OUTCOME: - # XXX Only first exception will be seen by user, - # ideally all should be reported. - if exc is None: - exc = sys.exc_info() - if exc: - six.reraise(*exc) - - def prepare(self, colitem): - """ setup objects along the collector chain to the test-method - and teardown previously setup objects.""" - needed_collectors = colitem.listchain() - self._teardown_towards(needed_collectors) - - # check if the last collection node has raised an error - for col in self.stack: - if hasattr(col, "_prepare_exc"): - six.reraise(*col._prepare_exc) - for col in needed_collectors[len(self.stack) :]: - self.stack.append(col) - try: - col.setup() - except TEST_OUTCOME: - col._prepare_exc = sys.exc_info() - raise - - -def collect_one_node(collector): - ihook = collector.ihook - ihook.pytest_collectstart(collector=collector) - rep = 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 + longrepr = None + if not call.excinfo: + outcome = "passed" + else: + from _pytest import nose + + skip_exceptions = (Skipped,) + nose.get_skip_exceptions() + if call.excinfo.errisinstance(skip_exceptions): + outcome = "skipped" + r = collector._repr_failure_py(call.excinfo, "line").reprcrash + longrepr = (str(r.path), r.lineno, r.message) + else: + outcome = "failed" + errorinfo = collector.repr_failure(call.excinfo) + if not hasattr(errorinfo, "toterminal"): + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo + rep = CollectReport( + collector.nodeid, outcome, longrepr, getattr(call, "result", None) + ) + rep.call = call # see collect_one_node + return rep + + +class SetupState(object): + """ shared state for setting up/tearing down test items or collectors. """ + + def __init__(self): + self.stack = [] + self._finalizers = {} + + def addfinalizer(self, finalizer, colitem): + """ attach a finalizer to the given colitem. + if colitem is None, this will add a finalizer that + is called at the end of teardown_all(). + """ + 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): + finalizers = self._finalizers.pop(colitem, None) + exc = None + while finalizers: + fin = finalizers.pop() + try: + fin() + except TEST_OUTCOME: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if exc is None: + exc = sys.exc_info() + if exc: + six.reraise(*exc) + + def _teardown_with_finalization(self, colitem): + self._callfinalizers(colitem) + if hasattr(colitem, "teardown"): + colitem.teardown() + for colitem in self._finalizers: + assert ( + colitem is None or colitem in self.stack or isinstance(colitem, tuple) + ) + + def teardown_all(self): + 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): + needed_collectors = nextitem and nextitem.listchain() or [] + self._teardown_towards(needed_collectors) + + def _teardown_towards(self, needed_collectors): + exc = None + while self.stack: + if self.stack == needed_collectors[: len(self.stack)]: + break + try: + self._pop_and_teardown() + except TEST_OUTCOME: + # XXX Only first exception will be seen by user, + # ideally all should be reported. + if exc is None: + exc = sys.exc_info() + if exc: + six.reraise(*exc) + + def prepare(self, colitem): + """ setup objects along the collector chain to the test-method + and teardown previously setup objects.""" + needed_collectors = colitem.listchain() + self._teardown_towards(needed_collectors) + + # check if the last collection node has raised an error + for col in self.stack: + if hasattr(col, "_prepare_exc"): + six.reraise(*col._prepare_exc) + for col in needed_collectors[len(self.stack) :]: + self.stack.append(col) + try: + col.setup() + except TEST_OUTCOME: + col._prepare_exc = sys.exc_info() + raise + + +def collect_one_node(collector): + ihook = collector.ihook + ihook.pytest_collectstart(collector=collector) + rep = 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 diff --git a/contrib/python/pytest/py2/_pytest/setuponly.py b/contrib/python/pytest/py2/_pytest/setuponly.py index ee02bc0ceb..0859011241 100644 --- a/contrib/python/pytest/py2/_pytest/setuponly.py +++ b/contrib/python/pytest/py2/_pytest/setuponly.py @@ -1,89 +1,89 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import sys - -import pytest - - -def pytest_addoption(parser): - 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, request): - yield - config = request.config - if 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): - fixturedef.cached_param = fixturedef.ids(request.param) - else: - fixturedef.cached_param = fixturedef.ids[request.param_index] - else: - fixturedef.cached_param = request.param - _show_fixture_action(fixturedef, "SETUP") - - -def pytest_fixture_post_finalizer(fixturedef): - if hasattr(fixturedef, "cached_result"): - config = fixturedef._fixturemanager.config - if config.option.setupshow: - _show_fixture_action(fixturedef, "TEARDOWN") - if hasattr(fixturedef, "cached_param"): - del fixturedef.cached_param - - -def _show_fixture_action(fixturedef, msg): - config = fixturedef._fixturemanager.config - capman = config.pluginmanager.getplugin("capturemanager") - if capman: - capman.suspend_global_capture() - out, err = capman.read_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(fixturedef.cached_param)) - - if capman: - capman.resume_global_capture() - sys.stdout.write(out) - sys.stderr.write(err) - - -@pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config): - if config.option.setuponly: - config.option.setupshow = True +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys + +import pytest + + +def pytest_addoption(parser): + 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, request): + yield + config = request.config + if 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): + fixturedef.cached_param = fixturedef.ids(request.param) + else: + fixturedef.cached_param = fixturedef.ids[request.param_index] + else: + fixturedef.cached_param = request.param + _show_fixture_action(fixturedef, "SETUP") + + +def pytest_fixture_post_finalizer(fixturedef): + if hasattr(fixturedef, "cached_result"): + config = fixturedef._fixturemanager.config + if config.option.setupshow: + _show_fixture_action(fixturedef, "TEARDOWN") + if hasattr(fixturedef, "cached_param"): + del fixturedef.cached_param + + +def _show_fixture_action(fixturedef, msg): + config = fixturedef._fixturemanager.config + capman = config.pluginmanager.getplugin("capturemanager") + if capman: + capman.suspend_global_capture() + out, err = capman.read_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(fixturedef.cached_param)) + + if capman: + capman.resume_global_capture() + sys.stdout.write(out) + sys.stderr.write(err) + + +@pytest.hookimpl(tryfirst=True) +def pytest_cmdline_main(config): + if config.option.setuponly: + config.option.setupshow = True diff --git a/contrib/python/pytest/py2/_pytest/setupplan.py b/contrib/python/pytest/py2/_pytest/setupplan.py index 0d10515da4..47b0fe82ef 100644 --- a/contrib/python/pytest/py2/_pytest/setupplan.py +++ b/contrib/python/pytest/py2/_pytest/setupplan.py @@ -1,32 +1,32 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import pytest - - -def pytest_addoption(parser): - 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, request): - # Will return a dummy fixture if the setuponly option is provided. - if request.config.option.setupplan: - fixturedef.cached_result = (None, None, None) - return fixturedef.cached_result - - -@pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config): - if config.option.setupplan: - config.option.setuponly = True - config.option.setupshow = True +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import pytest + + +def pytest_addoption(parser): + 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, request): + # Will return a dummy fixture if the setuponly option is provided. + if request.config.option.setupplan: + fixturedef.cached_result = (None, None, None) + return fixturedef.cached_result + + +@pytest.hookimpl(tryfirst=True) +def pytest_cmdline_main(config): + if config.option.setupplan: + config.option.setuponly = True + config.option.setupshow = True diff --git a/contrib/python/pytest/py2/_pytest/skipping.py b/contrib/python/pytest/py2/_pytest/skipping.py index 03e233d8a2..bc8b88e717 100644 --- a/contrib/python/pytest/py2/_pytest/skipping.py +++ b/contrib/python/pytest/py2/_pytest/skipping.py @@ -1,186 +1,186 @@ # -*- coding: utf-8 -*- -""" support for skip/xfail functions and markers. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from _pytest.config import hookimpl -from _pytest.mark.evaluate import MarkEvaluator -from _pytest.outcomes import fail -from _pytest.outcomes import skip -from _pytest.outcomes import xfail - - -def pytest_addoption(parser): - group = parser.getgroup("general") - group.addoption( - "--runxfail", - action="store_true", - dest="runxfail", - default=False, +""" support for skip/xfail functions and markers. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +from _pytest.config import hookimpl +from _pytest.mark.evaluate import MarkEvaluator +from _pytest.outcomes import fail +from _pytest.outcomes import skip +from _pytest.outcomes import xfail + + +def pytest_addoption(parser): + 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", - ) - - -def pytest_configure(config): - 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 - 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): skip the given test function if eval(condition) " - "results in a True value. Evaluation happens within the " - "module global context. Example: skipif('sys.platform == \"win32\"') " - "skips the test if we are on the win32 platform. see " - "https://docs.pytest.org/en/latest/skipping.html", - ) - config.addinivalue_line( - "markers", - "xfail(condition, reason=None, run=True, raises=None, strict=False): " - "mark the test function as an expected failure if eval(condition) " - "has a True value. 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 " - "a true failure. See https://docs.pytest.org/en/latest/skipping.html", - ) - - -@hookimpl(tryfirst=True) -def pytest_runtest_setup(item): - # Check if skip or skipif are specified as pytest marks - item._skipped_by_mark = False - eval_skipif = MarkEvaluator(item, "skipif") - if eval_skipif.istrue(): - item._skipped_by_mark = True - skip(eval_skipif.getexplanation()) - - for skip_info in item.iter_markers(name="skip"): - item._skipped_by_mark = True - if "reason" in skip_info.kwargs: - skip(skip_info.kwargs["reason"]) - elif skip_info.args: - skip(skip_info.args[0]) - else: - skip("unconditional skip") - - item._evalxfail = MarkEvaluator(item, "xfail") - check_xfail_no_run(item) - - -@hookimpl(hookwrapper=True) -def pytest_pyfunc_call(pyfuncitem): - check_xfail_no_run(pyfuncitem) - outcome = yield - passed = outcome.excinfo is None - if passed: - check_strict_xfail(pyfuncitem) - - -def check_xfail_no_run(item): - """check xfail(run=False)""" - if not item.config.option.runxfail: - evalxfail = item._evalxfail - if evalxfail.istrue(): - if not evalxfail.get("run", True): - xfail("[NOTRUN] " + evalxfail.getexplanation()) - - -def check_strict_xfail(pyfuncitem): - """check xfail(strict=True) for the given PASSING test""" - evalxfail = pyfuncitem._evalxfail - if evalxfail.istrue(): - strict_default = pyfuncitem.config.getini("xfail_strict") - is_strict_xfail = evalxfail.get("strict", strict_default) - if is_strict_xfail: - del pyfuncitem._evalxfail - explanation = evalxfail.getexplanation() - fail("[XPASS(strict)] " + explanation, pytrace=False) - - -@hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item, call): - outcome = yield - rep = outcome.get_result() - evalxfail = getattr(item, "_evalxfail", None) - # unitttest special case, see setting of _unexpectedsuccess - if hasattr(item, "_unexpectedsuccess") and rep.when == "call": - from _pytest.compat import _is_unittest_unexpected_success_a_failure - - if item._unexpectedsuccess: - rep.longrepr = "Unexpected success: {}".format(item._unexpectedsuccess) - else: - rep.longrepr = "Unexpected success" - if _is_unittest_unexpected_success_a_failure(): - rep.outcome = "failed" - else: - rep.outcome = "passed" - rep.wasxfail = rep.longrepr - elif item.config.option.runxfail: - pass # don't interefere - elif call.excinfo and call.excinfo.errisinstance(xfail.Exception): - rep.wasxfail = "reason: " + call.excinfo.value.msg - rep.outcome = "skipped" - elif evalxfail and not rep.skipped and evalxfail.wasvalid() and evalxfail.istrue(): - if call.excinfo: - if evalxfail.invalidraise(call.excinfo.value): - rep.outcome = "failed" - else: - rep.outcome = "skipped" - rep.wasxfail = evalxfail.getexplanation() - elif call.when == "call": - strict_default = item.config.getini("xfail_strict") - is_strict_xfail = evalxfail.get("strict", strict_default) - explanation = evalxfail.getexplanation() - if is_strict_xfail: - rep.outcome = "failed" - rep.longrepr = "[XPASS(strict)] {}".format(explanation) - else: - rep.outcome = "passed" - rep.wasxfail = explanation - elif ( - getattr(item, "_skipped_by_mark", False) - 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 - # the location of where the skip exception was raised within pytest - filename, line, reason = rep.longrepr - filename, line = item.location[:2] - rep.longrepr = filename, line, reason - - -# called by terminalreporter progress reporting - - -def pytest_report_teststatus(report): - if hasattr(report, "wasxfail"): - if report.skipped: + ) + + 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): + 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 + 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): skip the given test function if eval(condition) " + "results in a True value. Evaluation happens within the " + "module global context. Example: skipif('sys.platform == \"win32\"') " + "skips the test if we are on the win32 platform. see " + "https://docs.pytest.org/en/latest/skipping.html", + ) + config.addinivalue_line( + "markers", + "xfail(condition, reason=None, run=True, raises=None, strict=False): " + "mark the test function as an expected failure if eval(condition) " + "has a True value. 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 " + "a true failure. See https://docs.pytest.org/en/latest/skipping.html", + ) + + +@hookimpl(tryfirst=True) +def pytest_runtest_setup(item): + # Check if skip or skipif are specified as pytest marks + item._skipped_by_mark = False + eval_skipif = MarkEvaluator(item, "skipif") + if eval_skipif.istrue(): + item._skipped_by_mark = True + skip(eval_skipif.getexplanation()) + + for skip_info in item.iter_markers(name="skip"): + item._skipped_by_mark = True + if "reason" in skip_info.kwargs: + skip(skip_info.kwargs["reason"]) + elif skip_info.args: + skip(skip_info.args[0]) + else: + skip("unconditional skip") + + item._evalxfail = MarkEvaluator(item, "xfail") + check_xfail_no_run(item) + + +@hookimpl(hookwrapper=True) +def pytest_pyfunc_call(pyfuncitem): + check_xfail_no_run(pyfuncitem) + outcome = yield + passed = outcome.excinfo is None + if passed: + check_strict_xfail(pyfuncitem) + + +def check_xfail_no_run(item): + """check xfail(run=False)""" + if not item.config.option.runxfail: + evalxfail = item._evalxfail + if evalxfail.istrue(): + if not evalxfail.get("run", True): + xfail("[NOTRUN] " + evalxfail.getexplanation()) + + +def check_strict_xfail(pyfuncitem): + """check xfail(strict=True) for the given PASSING test""" + evalxfail = pyfuncitem._evalxfail + if evalxfail.istrue(): + strict_default = pyfuncitem.config.getini("xfail_strict") + is_strict_xfail = evalxfail.get("strict", strict_default) + if is_strict_xfail: + del pyfuncitem._evalxfail + explanation = evalxfail.getexplanation() + fail("[XPASS(strict)] " + explanation, pytrace=False) + + +@hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + rep = outcome.get_result() + evalxfail = getattr(item, "_evalxfail", None) + # unitttest special case, see setting of _unexpectedsuccess + if hasattr(item, "_unexpectedsuccess") and rep.when == "call": + from _pytest.compat import _is_unittest_unexpected_success_a_failure + + if item._unexpectedsuccess: + rep.longrepr = "Unexpected success: {}".format(item._unexpectedsuccess) + else: + rep.longrepr = "Unexpected success" + if _is_unittest_unexpected_success_a_failure(): + rep.outcome = "failed" + else: + rep.outcome = "passed" + rep.wasxfail = rep.longrepr + elif item.config.option.runxfail: + pass # don't interefere + elif call.excinfo and call.excinfo.errisinstance(xfail.Exception): + rep.wasxfail = "reason: " + call.excinfo.value.msg + rep.outcome = "skipped" + elif evalxfail and not rep.skipped and evalxfail.wasvalid() and evalxfail.istrue(): + if call.excinfo: + if evalxfail.invalidraise(call.excinfo.value): + rep.outcome = "failed" + else: + rep.outcome = "skipped" + rep.wasxfail = evalxfail.getexplanation() + elif call.when == "call": + strict_default = item.config.getini("xfail_strict") + is_strict_xfail = evalxfail.get("strict", strict_default) + explanation = evalxfail.getexplanation() + if is_strict_xfail: + rep.outcome = "failed" + rep.longrepr = "[XPASS(strict)] {}".format(explanation) + else: + rep.outcome = "passed" + rep.wasxfail = explanation + elif ( + getattr(item, "_skipped_by_mark", False) + 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 + # the location of where the skip exception was raised within pytest + filename, line, reason = rep.longrepr + filename, line = item.location[:2] + rep.longrepr = filename, line, reason + + +# called by terminalreporter progress reporting + + +def pytest_report_teststatus(report): + if hasattr(report, "wasxfail"): + if report.skipped: return "xfailed", "x", "XFAIL" - elif report.passed: + elif report.passed: return "xpassed", "X", "XPASS" diff --git a/contrib/python/pytest/py2/_pytest/stepwise.py b/contrib/python/pytest/py2/_pytest/stepwise.py index f18cb61085..8890259589 100644 --- a/contrib/python/pytest/py2/_pytest/stepwise.py +++ b/contrib/python/pytest/py2/_pytest/stepwise.py @@ -1,109 +1,109 @@ # -*- coding: utf-8 -*- -import pytest - - -def pytest_addoption(parser): - group = parser.getgroup("general") - group.addoption( - "--sw", - "--stepwise", - action="store_true", - dest="stepwise", +import pytest + + +def pytest_addoption(parser): + group = parser.getgroup("general") + group.addoption( + "--sw", + "--stepwise", + action="store_true", + dest="stepwise", help="exit on test failure and continue from last failing test next time", - ) - group.addoption( - "--stepwise-skip", - action="store_true", - dest="stepwise_skip", - help="ignore the first failing test but stop on the next failing test", - ) - - -@pytest.hookimpl -def pytest_configure(config): - config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") - - -class StepwisePlugin: - def __init__(self, config): - self.config = config - self.active = config.getvalue("stepwise") - self.session = None + ) + group.addoption( + "--stepwise-skip", + action="store_true", + dest="stepwise_skip", + help="ignore the first failing test but stop on the next failing test", + ) + + +@pytest.hookimpl +def pytest_configure(config): + config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") + + +class StepwisePlugin: + def __init__(self, config): + self.config = config + self.active = config.getvalue("stepwise") + self.session = None self.report_status = "" - - if self.active: - self.lastfailed = config.cache.get("cache/stepwise", None) - self.skip = config.getvalue("stepwise_skip") - - def pytest_sessionstart(self, session): - self.session = session - - def pytest_collection_modifyitems(self, session, config, items): + + if self.active: + self.lastfailed = config.cache.get("cache/stepwise", None) + self.skip = config.getvalue("stepwise_skip") + + def pytest_sessionstart(self, session): + self.session = session + + def pytest_collection_modifyitems(self, session, config, items): if not self.active: - return + return if not self.lastfailed: self.report_status = "no previously failed tests, not skipping." return - - already_passed = [] - found = False - - # Make a list of all tests that have been run before the last failing one. - for item in items: - if item.nodeid == self.lastfailed: - found = True - break - else: - already_passed.append(item) - - # If the previously failed test was not found among the test items, - # do not skip any tests. - if not found: + + already_passed = [] + found = False + + # Make a list of all tests that have been run before the last failing one. + for item in items: + if item.nodeid == self.lastfailed: + found = True + break + else: + already_passed.append(item) + + # If the previously failed test was not found among the test items, + # do not skip any tests. + if not found: self.report_status = "previously failed test not found, not skipping." - already_passed = [] + already_passed = [] else: self.report_status = "skipping {} already passed items.".format( len(already_passed) ) - - for item in already_passed: - items.remove(item) - - config.hook.pytest_deselected(items=already_passed) - - def pytest_runtest_logreport(self, report): + + for item in already_passed: + items.remove(item) + + config.hook.pytest_deselected(items=already_passed) + + def pytest_runtest_logreport(self, report): if not self.active: - return - - 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 - 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 - + return + + 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 + 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): if self.active and self.config.getoption("verbose") >= 0 and self.report_status: return "stepwise: %s" % self.report_status - def pytest_sessionfinish(self, session): - if self.active: - self.config.cache.set("cache/stepwise", self.lastfailed) - else: - # Clear the list of failing tests if the plugin is not active. - self.config.cache.set("cache/stepwise", []) + def pytest_sessionfinish(self, session): + if self.active: + self.config.cache.set("cache/stepwise", self.lastfailed) + else: + # Clear the list of failing tests if the plugin is not active. + self.config.cache.set("cache/stepwise", []) diff --git a/contrib/python/pytest/py2/_pytest/terminal.py b/contrib/python/pytest/py2/_pytest/terminal.py index 61163d2e51..4418338c65 100644 --- a/contrib/python/pytest/py2/_pytest/terminal.py +++ b/contrib/python/pytest/py2/_pytest/terminal.py @@ -1,171 +1,171 @@ # -*- coding: utf-8 -*- -""" terminal reporting of the full testing process. - -This is a good source for looking at the various reporting hooks. -""" -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import argparse +""" terminal reporting of the full testing process. + +This is a good source for looking at the various reporting hooks. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse import collections -import platform -import sys -import time +import platform +import sys +import time from functools import partial - -import attr -import pluggy -import py -import six -from more_itertools import collapse - -import pytest -from _pytest import nodes -from _pytest.main import EXIT_INTERRUPTED -from _pytest.main import EXIT_NOTESTSCOLLECTED -from _pytest.main import EXIT_OK -from _pytest.main import EXIT_TESTSFAILED -from _pytest.main import EXIT_USAGEERROR - + +import attr +import pluggy +import py +import six +from more_itertools import collapse + +import pytest +from _pytest import nodes +from _pytest.main import EXIT_INTERRUPTED +from _pytest.main import EXIT_NOTESTSCOLLECTED +from _pytest.main import EXIT_OK +from _pytest.main import EXIT_TESTSFAILED +from _pytest.main import EXIT_USAGEERROR + REPORT_COLLECTING_RESOLUTION = 0.5 - - -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, dest, default=None, required=False, help=None): - super(MoreQuietAction, self).__init__( - option_strings=option_strings, - dest=dest, - nargs=0, - default=default, - required=required, - help=help, - ) - - def __call__(self, parser, namespace, values, option_string=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 - - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting", "reporting", after="general") - group._addoption( - "-v", - "--verbose", - action="count", - default=0, - dest="verbose", - help="increase verbosity.", - ), - group._addoption( - "-q", - "--quiet", - action=MoreQuietAction, - default=0, - dest="verbose", - help="decrease verbosity.", - ), - group._addoption( - "--verbosity", dest="verbose", type=int, default=0, help="set verbosity" - ) - group._addoption( - "-r", - action="store", - dest="reportchars", - default="", - metavar="chars", + + +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, dest, default=None, required=False, help=None): + super(MoreQuietAction, self).__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + default=default, + required=required, + help=help, + ) + + def __call__(self, parser, namespace, values, option_string=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 + + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting", "reporting", after="general") + group._addoption( + "-v", + "--verbose", + action="count", + default=0, + dest="verbose", + help="increase verbosity.", + ), + group._addoption( + "-q", + "--quiet", + action=MoreQuietAction, + default=0, + dest="verbose", + help="decrease verbosity.", + ), + group._addoption( + "--verbosity", dest="verbose", type=int, default=0, help="set verbosity" + ) + group._addoption( + "-r", + action="store", + dest="reportchars", + default="", + 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. " - "Warnings are displayed at all times except when " + "Warnings are displayed at all times except when " "--disable-warnings is set.", - ) - 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).", - ) - - parser.addini( - "console_output_style", + ) + 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).", + ) + + parser.addini( + "console_output_style", help='console output: "classic", or with additional progress information ("progress" (percentage) | "count").', - default="progress", - ) - - -def pytest_configure(config): - 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): - reportopts = "" - reportchars = config.option.reportchars - if not config.option.disable_warnings and "w" not in reportchars: - reportchars += "w" - elif config.option.disable_warnings and "w" in reportchars: - reportchars = reportchars.replace("w", "") + default="progress", + ) + + +def pytest_configure(config): + 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): + reportopts = "" + reportchars = config.option.reportchars + if not config.option.disable_warnings and "w" not in reportchars: + reportchars += "w" + elif config.option.disable_warnings and "w" in reportchars: + reportchars = reportchars.replace("w", "") aliases = {"F", "S"} for char in reportchars: # handle old aliases @@ -178,98 +178,98 @@ def getreportopt(config): break elif char not in reportopts: reportopts += char - return reportopts - - + return reportopts + + @pytest.hookimpl(trylast=True) # after _pytest.runner -def pytest_report_teststatus(report): +def pytest_report_teststatus(report): letter = "F" - if report.passed: - letter = "." - elif report.skipped: - letter = "s" - + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + outcome = report.outcome if report.when in ("collect", "setup", "teardown") and outcome == "failed": outcome = "error" letter = "E" - + return outcome, letter, outcome.upper() -@attr.s -class WarningReport(object): - """ +@attr.s +class WarningReport(object): + """ Simple structure to hold warnings information captured by ``pytest_warning_captured``. - - :ivar str message: user friendly message about the warning - :ivar str|None nodeid: node id that generated the warning (see ``get_location``). - :ivar tuple|py.path.local fslocation: - file system location of the source of the warning (see ``get_location``). - """ - - message = attr.ib() - nodeid = attr.ib(default=None) - fslocation = attr.ib(default=None) + + :ivar str message: user friendly message about the warning + :ivar str|None nodeid: node id that generated the warning (see ``get_location``). + :ivar tuple|py.path.local fslocation: + file system location of the source of the warning (see ``get_location``). + """ + + message = attr.ib() + nodeid = attr.ib(default=None) + fslocation = attr.ib(default=None) count_towards_summary = True - - def get_location(self, config): - """ - Returns 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] - relpath = py.path.local(filename).relto(config.invocation_dir) - if not relpath: - relpath = str(filename) - return "%s:%s" % (relpath, linenum) - else: - return str(self.fslocation) - return None - - -class TerminalReporter(object): - def __init__(self, config, file=None): - import _pytest.config - - self.config = config - self._numcollected = 0 - self._session = None + + def get_location(self, config): + """ + Returns 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] + relpath = py.path.local(filename).relto(config.invocation_dir) + if not relpath: + relpath = str(filename) + return "%s:%s" % (relpath, linenum) + else: + return str(self.fslocation) + return None + + +class TerminalReporter(object): + def __init__(self, config, file=None): + import _pytest.config + + self.config = config + self._numcollected = 0 + self._session = None self._showfspath = None - - self.stats = {} + + self.stats = {} self.startdir = config.invocation_dir - if file is None: - file = sys.stdout - self._tw = _pytest.config.create_terminal_writer(config, file) - # self.writer will be deprecated in pytest-3.4 - self.writer = self._tw - self._screen_width = self._tw.fullwidth - self.currentfspath = None - self.reportchars = getreportopt(config) - self.hasmarkup = self._tw.hasmarkup - self.isatty = file.isatty() - self._progress_nodeids_reported = set() - self._show_progress_info = self._determine_show_progress_info() - self._collect_report_last_write = None - - def _determine_show_progress_info(self): - """Return True if we should display progress information based on the current config""" - # do not show progress if we are not capturing output (#3038) + if file is None: + file = sys.stdout + self._tw = _pytest.config.create_terminal_writer(config, file) + # self.writer will be deprecated in pytest-3.4 + self.writer = self._tw + self._screen_width = self._tw.fullwidth + self.currentfspath = None + self.reportchars = getreportopt(config) + self.hasmarkup = self._tw.hasmarkup + self.isatty = file.isatty() + self._progress_nodeids_reported = set() + self._show_progress_info = self._determine_show_progress_info() + self._collect_report_last_write = None + + def _determine_show_progress_info(self): + """Return True if we should display progress information based on the current config""" + # 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 = self.config.getini("console_output_style") if cfg in ("progress", "count"): return cfg return False - + @property def verbosity(self): return self.config.option.verbose @@ -292,503 +292,503 @@ class TerminalReporter(object): def showlongtestinfo(self): return self.verbosity > 0 - def hasopt(self, char): - char = {"xfailed": "x", "skipped": "s"}.get(char, char) - return char in self.reportchars - - def write_fspath_result(self, nodeid, res, **markup): - fspath = self.config.rootdir.join(nodeid.split("::")[0]) + def hasopt(self, char): + char = {"xfailed": "x", "skipped": "s"}.get(char, char) + return char in self.reportchars + + def write_fspath_result(self, nodeid, res, **markup): + fspath = self.config.rootdir.join(nodeid.split("::")[0]) # NOTE: explicitly check for None to work around py bug, and for less # overhead in general (https://github.com/pytest-dev/py/pull/207). 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 - fspath = self.startdir.bestrelpath(fspath) - self._tw.line() - self._tw.write(fspath + " ") - self._tw.write(res, **markup) - - def write_ensure_prefix(self, prefix, extra="", **kwargs): - 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): - if self.currentfspath: - self._tw.line() - self.currentfspath = None - - def write(self, content, **markup): - self._tw.write(content, **markup) - - def write_line(self, line, **markup): - if not isinstance(line, six.text_type): - line = six.text_type(line, errors="replace") - self.ensure_newline() - self._tw.line(line, **markup) - - def rewrite(self, line, **markup): - """ - Rewinds the terminal cursor to the beginning and writes the given line. - - :kwarg erase: if True, will also add spaces until the full terminal width to ensure - 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, title=None, **markup): - self.ensure_newline() - self._tw.sep(sep, title, **markup) - - def section(self, title, sep="=", **kw): - self._tw.sep(sep, title, **kw) - - def line(self, msg, **kw): - self._tw.line(msg, **kw) - - def pytest_internalerror(self, excrepr): - for line in six.text_type(excrepr).split("\n"): - self.write_line("INTERNALERROR> " + line) - return 1 - - def pytest_warning_captured(self, warning_message, item): - # from _pytest.nodes import get_fslocation_from_item - from _pytest.warnings import warning_record_to_str - - warnings = self.stats.setdefault("warnings", []) - fslocation = warning_message.filename, warning_message.lineno - message = warning_record_to_str(warning_message) - - nodeid = item.nodeid if item is not None else "" - warning_report = WarningReport( - fslocation=fslocation, message=message, nodeid=nodeid - ) - warnings.append(warning_report) - - def pytest_plugin_registered(self, plugin): - if self.config.option.traceconfig: - msg = "PLUGIN registered: %s" % (plugin,) - # XXX this event may happen during setup/teardown time - # which unfortunately captures our output here - # which garbles our output if we use self.write_line - self.write_line(msg) - - def pytest_deselected(self, items): - self.stats.setdefault("deselected", []).extend(items) - - def pytest_runtest_logstart(self, nodeid, location): - # 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, "") - elif self.showfspath: - fsid = nodeid.split("::")[0] - self.write_fspath_result(fsid, "") - - def pytest_runtest_logreport(self, report): + if self.currentfspath is not None and self._show_progress_info: + self._write_progress_information_filling_space() + self.currentfspath = fspath + fspath = self.startdir.bestrelpath(fspath) + self._tw.line() + self._tw.write(fspath + " ") + self._tw.write(res, **markup) + + def write_ensure_prefix(self, prefix, extra="", **kwargs): + 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): + if self.currentfspath: + self._tw.line() + self.currentfspath = None + + def write(self, content, **markup): + self._tw.write(content, **markup) + + def write_line(self, line, **markup): + if not isinstance(line, six.text_type): + line = six.text_type(line, errors="replace") + self.ensure_newline() + self._tw.line(line, **markup) + + def rewrite(self, line, **markup): + """ + Rewinds the terminal cursor to the beginning and writes the given line. + + :kwarg erase: if True, will also add spaces until the full terminal width to ensure + 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, title=None, **markup): + self.ensure_newline() + self._tw.sep(sep, title, **markup) + + def section(self, title, sep="=", **kw): + self._tw.sep(sep, title, **kw) + + def line(self, msg, **kw): + self._tw.line(msg, **kw) + + def pytest_internalerror(self, excrepr): + for line in six.text_type(excrepr).split("\n"): + self.write_line("INTERNALERROR> " + line) + return 1 + + def pytest_warning_captured(self, warning_message, item): + # from _pytest.nodes import get_fslocation_from_item + from _pytest.warnings import warning_record_to_str + + warnings = self.stats.setdefault("warnings", []) + fslocation = warning_message.filename, warning_message.lineno + message = warning_record_to_str(warning_message) + + nodeid = item.nodeid if item is not None else "" + warning_report = WarningReport( + fslocation=fslocation, message=message, nodeid=nodeid + ) + warnings.append(warning_report) + + def pytest_plugin_registered(self, plugin): + if self.config.option.traceconfig: + msg = "PLUGIN registered: %s" % (plugin,) + # XXX this event may happen during setup/teardown time + # which unfortunately captures our output here + # which garbles our output if we use self.write_line + self.write_line(msg) + + def pytest_deselected(self, items): + self.stats.setdefault("deselected", []).extend(items) + + def pytest_runtest_logstart(self, nodeid, location): + # 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, "") + elif self.showfspath: + fsid = nodeid.split("::")[0] + self.write_fspath_result(fsid, "") + + def pytest_runtest_logreport(self, report): self._tests_ran = True - rep = report + rep = report res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) - category, letter, word = res - if isinstance(word, tuple): - word, markup = word - else: - markup = None - self.stats.setdefault(category, []).append(rep) - if not letter and not word: - # probably passed setup/teardown - return - running_xdist = hasattr(rep, "node") - if markup is None: + category, letter, word = res + if isinstance(word, tuple): + word, markup = word + else: + markup = None + self.stats.setdefault(category, []).append(rep) + if not letter and not word: + # probably passed setup/teardown + 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: - if not running_xdist and self.showfspath: - self.write_fspath_result(rep.nodeid, letter, **markup) - else: - 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) - 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 - - def pytest_runtest_logfinish(self, nodeid): + elif rep.failed: + markup = {"red": True} + elif rep.skipped: + markup = {"yellow": True} + else: + markup = {} + if self.verbosity <= 0: + if not running_xdist and self.showfspath: + self.write_fspath_result(rep.nodeid, letter, **markup) + else: + 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) + 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 + + def pytest_runtest_logfinish(self, nodeid): if self.verbosity <= 0 and self._show_progress_info: if self._show_progress_info == "count": num_tests = self._session.testscollected 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) is_last_item = ( - len(self._progress_nodeids_reported) == self._session.testscollected - ) + len(self._progress_nodeids_reported) == self._session.testscollected + ) if is_last_item: - self._write_progress_information_filling_space() - else: - 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", cyan=True) - - def _get_progress_information_message(self): - collected = self._session.testscollected + self._write_progress_information_filling_space() + else: + 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", cyan=True) + + def _get_progress_information_message(self): + collected = self._session.testscollected if self._show_progress_info == "count": - if collected: - progress = self._progress_nodeids_reported - counter_format = "{{:{}d}}".format(len(str(collected))) - format_string = " [{}/{{}}]".format(counter_format) - return format_string.format(len(progress), collected) - return " [ {} / {} ]".format(collected, collected) - else: - if collected: - progress = len(self._progress_nodeids_reported) * 100 // collected - return " [{:3d}%]".format(progress) - return " [100%]" - - def _write_progress_information_filling_space(self): - msg = self._get_progress_information_message() - w = self._width_of_current_line - fill = self._tw.fullwidth - w - 1 - self.write(msg.rjust(fill), cyan=True) - - @property - def _width_of_current_line(self): - """Return the width of current line, using the superior implementation of py-1.6 when available""" - try: - return self._tw.width_of_current_line - except AttributeError: - # py < 1.6.0 - return self._tw.chars_on_current_line - - def pytest_collection(self): - if self.isatty: - if self.config.option.verbose >= 0: - self.write("collecting ... ", bold=True) - self._collect_report_last_write = time.time() - elif self.config.option.verbose >= 1: - self.write("collecting ... ", bold=True) - - def pytest_collectreport(self, report): - if report.failed: - self.stats.setdefault("error", []).append(report) - elif report.skipped: - self.stats.setdefault("skipped", []).append(report) - items = [x for x in report.result if isinstance(x, pytest.Item)] - self._numcollected += len(items) - if self.isatty: - self.report_collect() - - def report_collect(self, final=False): - if self.config.option.verbose < 0: - return - - if not final: - # Only write "collecting" report every 0.5s. - t = time.time() - if ( - self._collect_report_last_write is not None + if collected: + progress = self._progress_nodeids_reported + counter_format = "{{:{}d}}".format(len(str(collected))) + format_string = " [{}/{{}}]".format(counter_format) + return format_string.format(len(progress), collected) + return " [ {} / {} ]".format(collected, collected) + else: + if collected: + progress = len(self._progress_nodeids_reported) * 100 // collected + return " [{:3d}%]".format(progress) + return " [100%]" + + def _write_progress_information_filling_space(self): + msg = self._get_progress_information_message() + w = self._width_of_current_line + fill = self._tw.fullwidth - w - 1 + self.write(msg.rjust(fill), cyan=True) + + @property + def _width_of_current_line(self): + """Return the width of current line, using the superior implementation of py-1.6 when available""" + try: + return self._tw.width_of_current_line + except AttributeError: + # py < 1.6.0 + return self._tw.chars_on_current_line + + def pytest_collection(self): + if self.isatty: + if self.config.option.verbose >= 0: + self.write("collecting ... ", bold=True) + self._collect_report_last_write = time.time() + elif self.config.option.verbose >= 1: + self.write("collecting ... ", bold=True) + + def pytest_collectreport(self, report): + if report.failed: + self.stats.setdefault("error", []).append(report) + elif report.skipped: + self.stats.setdefault("skipped", []).append(report) + items = [x for x in report.result if isinstance(x, pytest.Item)] + self._numcollected += len(items) + if self.isatty: + self.report_collect() + + def report_collect(self, final=False): + if self.config.option.verbose < 0: + return + + if not final: + # Only write "collecting" report every 0.5s. + t = time.time() + 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: - line += " / %d errors" % errors - if deselected: - line += " / %d deselected" % deselected - if skipped: - line += " / %d skipped" % skipped + if final: + line = "collected " + else: + line = "collecting " + line += ( + str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") + ) + if errors: + line += " / %d errors" % errors + 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) - - @pytest.hookimpl(trylast=True) - def pytest_sessionstart(self, session): - self._session = session - self._sessionstarttime = time.time() - if not self.showheader: - return - self.write_sep("=", "test session starts", bold=True) - verinfo = platform.python_version() - msg = "platform %s -- Python %s" % (sys.platform, verinfo) - if hasattr(sys, "pypy_version_info"): - verinfo = ".".join(map(str, sys.pypy_version_info[:3])) - msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3]) - msg += ", pytest-%s, py-%s, pluggy-%s" % ( - pytest.__version__, - py.__version__, - pluggy.__version__, - ) - if ( - self.verbosity > 0 - or self.config.option.debug - or getattr(self.config.option, "pastebin", None) - ): - msg += " -- " + str(sys.executable) - self.write_line(msg) - lines = self.config.hook.pytest_report_header( - config=self.config, startdir=self.startdir - ) - self._write_report_lines_from_hooks(lines) - - def _write_report_lines_from_hooks(self, lines): - lines.reverse() - for line in collapse(lines): - self.write_line(line) - - def pytest_report_header(self, config): + if self.isatty: + self.rewrite(line, bold=True, erase=True) + if final: + self.write("\n") + else: + self.write_line(line) + + @pytest.hookimpl(trylast=True) + def pytest_sessionstart(self, session): + self._session = session + self._sessionstarttime = time.time() + if not self.showheader: + return + self.write_sep("=", "test session starts", bold=True) + verinfo = platform.python_version() + msg = "platform %s -- Python %s" % (sys.platform, verinfo) + if hasattr(sys, "pypy_version_info"): + verinfo = ".".join(map(str, sys.pypy_version_info[:3])) + msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3]) + msg += ", pytest-%s, py-%s, pluggy-%s" % ( + pytest.__version__, + py.__version__, + pluggy.__version__, + ) + if ( + self.verbosity > 0 + or self.config.option.debug + or getattr(self.config.option, "pastebin", None) + ): + msg += " -- " + str(sys.executable) + self.write_line(msg) + lines = self.config.hook.pytest_report_header( + config=self.config, startdir=self.startdir + ) + self._write_report_lines_from_hooks(lines) + + def _write_report_lines_from_hooks(self, lines): + lines.reverse() + for line in collapse(lines): + self.write_line(line) + + def pytest_report_header(self, config): line = "rootdir: %s" % config.rootdir - if config.inifile: + if config.inifile: line += ", inifile: " + config.rootdir.bestrelpath(config.inifile) - + testpaths = config.getini("testpaths") if testpaths and config.args == testpaths: rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths] line += ", testpaths: {}".format(", ".join(rel_paths)) 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): self.report_collect(True) - + if self.config.getoption("collectonly"): - self._printcollecteditems(session.items) + self._printcollecteditems(session.items) + + 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 self.stats.get("failed"): self._tw.sep("!", "collection failures") for rep in self.stats.get("failed"): rep.toterminal(self._tw) - def _printcollecteditems(self, items): - # to print out items and their parent collectors - # 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: - counts = {} - for item in items: - name = item.nodeid.split("::", 1)[0] - counts[name] = counts.get(name, 0) + 1 - 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 = [] - 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("%s%s" % (indent, col)) + def _printcollecteditems(self, items): + # to print out items and their parent collectors + # 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: + counts = {} + for item in items: + name = item.nodeid.split("::", 1)[0] + counts[name] = counts.get(name, 0) + 1 + 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 = [] + 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("%s%s" % (indent, col)) if self.config.option.verbose >= 1: if hasattr(col, "_obj") and col._obj.__doc__: for line in col._obj.__doc__.strip().splitlines(): self._tw.line("%s%s" % (indent + " ", line.strip())) - - @pytest.hookimpl(hookwrapper=True) - def pytest_sessionfinish(self, exitstatus): - outcome = yield - outcome.get_result() - self._tw.line("") - summary_exit_codes = ( - EXIT_OK, - EXIT_TESTSFAILED, - EXIT_INTERRUPTED, - EXIT_USAGEERROR, - EXIT_NOTESTSCOLLECTED, - ) - if exitstatus in summary_exit_codes: - self.config.hook.pytest_terminal_summary( + + @pytest.hookimpl(hookwrapper=True) + def pytest_sessionfinish(self, exitstatus): + outcome = yield + outcome.get_result() + self._tw.line("") + summary_exit_codes = ( + EXIT_OK, + EXIT_TESTSFAILED, + EXIT_INTERRUPTED, + EXIT_USAGEERROR, + EXIT_NOTESTSCOLLECTED, + ) + if exitstatus in summary_exit_codes: + self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config - ) - if exitstatus == EXIT_INTERRUPTED: - self._report_keyboardinterrupt() - del self._keyboardinterrupt_memo - self.summary_stats() - - @pytest.hookimpl(hookwrapper=True) - def pytest_terminal_summary(self): - self.summary_errors() - self.summary_failures() - self.summary_warnings() + ) + if exitstatus == EXIT_INTERRUPTED: + self._report_keyboardinterrupt() + del self._keyboardinterrupt_memo + self.summary_stats() + + @pytest.hookimpl(hookwrapper=True) + def pytest_terminal_summary(self): + 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() - - def pytest_keyboard_interrupt(self, excinfo): - self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) - - def pytest_unconfigure(self): - if hasattr(self, "_keyboardinterrupt_memo"): - self._report_keyboardinterrupt() - - def _report_keyboardinterrupt(self): - excrepr = self._keyboardinterrupt_memo - 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 --fulltrace)", - 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 - ): - res += " <- " + self.startdir.bestrelpath(fspath) - else: - res = "[location]" - return res + " " - - def _getfailureheadline(self, rep): + # Display any extra warnings from teardown here (if any). + self.summary_warnings() + + def pytest_keyboard_interrupt(self, excinfo): + self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) + + def pytest_unconfigure(self): + if hasattr(self, "_keyboardinterrupt_memo"): + self._report_keyboardinterrupt() + + def _report_keyboardinterrupt(self): + excrepr = self._keyboardinterrupt_memo + 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 --fulltrace)", + 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 + ): + res += " <- " + self.startdir.bestrelpath(fspath) + 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 "" - - # - # summaries for sessionfinish - # - def getreports(self, name): - values = [] - for x in self.stats.get(name, []): - if not hasattr(x, "_pdbshown"): - values.append(x) - return values - - def summary_warnings(self): - if self.hasopt("w"): - all_warnings = self.stats.get("warnings") - if not all_warnings: - return - - final = hasattr(self, "_already_displayed_warnings") - if final: + + 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): + values = [] + for x in self.stats.get(name, []): + if not hasattr(x, "_pdbshown"): + values.append(x) + return values + + def summary_warnings(self): + if self.hasopt("w"): + all_warnings = self.stats.get("warnings") + if not all_warnings: + return + + final = hasattr(self, "_already_displayed_warnings") + 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 = collections.OrderedDict() for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) - - 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, warning_reports in reports_grouped_by_message.items(): has_any_location = False for w in warning_reports: location = w.get_location(self.config) - if location: + if location: self._tw.line(str(location)) has_any_location = True if has_any_location: @@ -798,45 +798,45 @@ class TerminalReporter(object): else: message = message.rstrip() self._tw.line(message) - self._tw.line() - self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") - - def summary_passes(self): - if self.config.option.tbstyle != "no": - if self.hasopt("P"): - reports = self.getreports("passed") - if not reports: - return - self.write_sep("=", "PASSES") - for rep in reports: - if rep.sections: - msg = self._getfailureheadline(rep) + self._tw.line() + self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html") + + def summary_passes(self): + if self.config.option.tbstyle != "no": + if self.hasopt("P"): + reports = self.getreports("passed") + 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) - - def print_teardown_sections(self, rep): - 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): - if self.config.option.tbstyle != "no": - reports = self.getreports("failed") - if not reports: - return - self.write_sep("=", "FAILURES") + self._outrep_summary(rep) + + def print_teardown_sections(self, rep): + 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): + if self.config.option.tbstyle != "no": + reports = self.getreports("failed") + 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: teardown_sections = {} for report in self.getreports(""): @@ -844,55 +844,55 @@ class TerminalReporter(object): teardown_sections.setdefault(report.nodeid, []).append(report) for rep in reports: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) for report in teardown_sections.get(rep.nodeid, []): self.print_teardown_sections(report) - - def summary_errors(self): - if self.config.option.tbstyle != "no": - reports = self.getreports("error") - if not reports: - return - self.write_sep("=", "ERRORS") - for rep in self.stats["error"]: - msg = self._getfailureheadline(rep) + + def summary_errors(self): + if self.config.option.tbstyle != "no": + reports = self.getreports("error") + 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 = "ERROR at %s of %s" % (rep.when, msg) - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) - - def _outrep_summary(self, rep): - 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): - session_duration = time.time() - self._sessionstarttime - (line, color) = build_summary_stats_line(self.stats) - msg = "%s in %.2f seconds" % (line, session_duration) - markup = {color: True, "bold": True} - - if self.verbosity >= 0: - self.write_sep("=", msg, **markup) - if self.verbosity == -1: - self.write_line(msg, **markup) - + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) + + def _outrep_summary(self, rep): + 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): + session_duration = time.time() - self._sessionstarttime + (line, color) = build_summary_stats_line(self.stats) + msg = "%s in %.2f seconds" % (line, session_duration) + markup = {color: True, "bold": True} + + if self.verbosity >= 0: + self.write_sep("=", msg, **markup) + if self.verbosity == -1: + self.write_line(msg, **markup) + def short_test_summary(self): if not self.reportchars: return - + def show_simple(stat, lines): failed = self.stats.get(stat, []) if not failed: @@ -978,7 +978,7 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): # No space for an additional message. return line - try: + try: msg = rep.longrepr.reprcrash.message except AttributeError: pass @@ -988,7 +988,7 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): if i != -1: msg = msg[:i] len_msg = wcswidth(msg) - + sep, len_sep = " - ", 3 max_len_msg = termwidth - len_line - len_sep if max_len_msg >= len_ellipsis: @@ -1015,7 +1015,7 @@ def _get_line_with_reprcrash_message(config, rep, termwidth): msg += ellipsis line += sep + msg return line - + def _folded_skips(skipped): d = {} @@ -1039,7 +1039,7 @@ def _folded_skips(skipped): return values -def build_summary_stats_line(stats): +def build_summary_stats_line(stats): known_types = ( "failed passed skipped deselected xfailed xpassed warnings error".split() ) @@ -1049,7 +1049,7 @@ def build_summary_stats_line(stats): if found_type: # setup/teardown reports have an empty key, ignore them known_types.append(found_type) unknown_type_seen = True - parts = [] + parts = [] for key in known_types: reports = stats.get(key, None) if reports: @@ -1057,34 +1057,34 @@ def build_summary_stats_line(stats): 1 for rep in reports if getattr(rep, "count_towards_summary", True) ) parts.append("%d %s" % (count, key)) - - if parts: - line = ", ".join(parts) - else: - line = "no tests ran" - - if "failed" in stats or "error" in stats: - color = "red" + + if parts: + line = ", ".join(parts) + else: + line = "no tests ran" + + if "failed" in stats or "error" in stats: + color = "red" elif "warnings" in stats or unknown_type_seen: - color = "yellow" - elif "passed" in stats: - color = "green" - else: - color = "yellow" - + color = "yellow" + elif "passed" in stats: + color = "green" + else: + color = "yellow" + return line, color - - -def _plugin_nameversions(plugininfo): - values = [] - for plugin, dist in plugininfo: - # gets us name and version! - name = "{dist.project_name}-{dist.version}".format(dist=dist) - # questionable convenience, but it keeps things short - 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 + + +def _plugin_nameversions(plugininfo): + values = [] + for plugin, dist in plugininfo: + # gets us name and version! + name = "{dist.project_name}-{dist.version}".format(dist=dist) + # questionable convenience, but it keeps things short + 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 diff --git a/contrib/python/pytest/py2/_pytest/tmpdir.py b/contrib/python/pytest/py2/_pytest/tmpdir.py index d146cefa2b..a8a7037713 100644 --- a/contrib/python/pytest/py2/_pytest/tmpdir.py +++ b/contrib/python/pytest/py2/_pytest/tmpdir.py @@ -1,68 +1,68 @@ # -*- coding: utf-8 -*- -""" support for providing temporary directories to test functions. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import os -import re -import tempfile -import warnings - -import attr -import py -import six - -import pytest -from .pathlib import ensure_reset_dir -from .pathlib import LOCK_TIMEOUT -from .pathlib import make_numbered_dir -from .pathlib import make_numbered_dir_with_cleanup -from .pathlib import Path -from _pytest.monkeypatch import MonkeyPatch - - -@attr.s -class TempPathFactory(object): - """Factory for temporary directories under the common base temp directory. - - The base directory can be configured using the ``--basetemp`` option.""" - - _given_basetemp = attr.ib( - # using os.path.abspath() to get absolute path instead of resolve() as it - # does not work the same in all platforms (see #4427) - # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012) +""" support for providing temporary directories to test functions. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import os +import re +import tempfile +import warnings + +import attr +import py +import six + +import pytest +from .pathlib import ensure_reset_dir +from .pathlib import LOCK_TIMEOUT +from .pathlib import make_numbered_dir +from .pathlib import make_numbered_dir_with_cleanup +from .pathlib import Path +from _pytest.monkeypatch import MonkeyPatch + + +@attr.s +class TempPathFactory(object): + """Factory for temporary directories under the common base temp directory. + + The base directory can be configured using the ``--basetemp`` option.""" + + _given_basetemp = attr.ib( + # using os.path.abspath() to get absolute path instead of resolve() as it + # does not work the same in all platforms (see #4427) + # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012) converter=attr.converters.optional( - lambda p: Path(os.path.abspath(six.text_type(p))) - ) - ) - _trace = attr.ib() - _basetemp = attr.ib(default=None) - - @classmethod - def from_config(cls, config): - """ - :param config: a pytest configuration - """ - return cls( - given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") - ) - - def mktemp(self, basename, numbered=True): - """makes a temporary directory managed by the factory""" - if not numbered: - p = self.getbasetemp().joinpath(basename) - p.mkdir() - else: - p = make_numbered_dir(root=self.getbasetemp(), prefix=basename) - self._trace("mktemp", p) - return p - - def getbasetemp(self): - """ return base temporary directory. """ + lambda p: Path(os.path.abspath(six.text_type(p))) + ) + ) + _trace = attr.ib() + _basetemp = attr.ib(default=None) + + @classmethod + def from_config(cls, config): + """ + :param config: a pytest configuration + """ + return cls( + given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") + ) + + def mktemp(self, basename, numbered=True): + """makes a temporary directory managed by the factory""" + if not numbered: + p = self.getbasetemp().joinpath(basename) + p.mkdir() + else: + p = make_numbered_dir(root=self.getbasetemp(), prefix=basename) + self._trace("mktemp", p) + return p + + def getbasetemp(self): + """ return base temporary directory. """ if self._basetemp is not None: - return self._basetemp - + return self._basetemp + if self._given_basetemp is not None: basetemp = self._given_basetemp ensure_reset_dir(basetemp) @@ -82,116 +82,116 @@ class TempPathFactory(object): self._basetemp = t = basetemp self._trace("new basetemp", t) return t - - -@attr.s -class TempdirFactory(object): - """ - backward comptibility wrapper that implements - :class:``py.path.local`` for :class:``TempPathFactory`` - """ - - _tmppath_factory = attr.ib() - - def ensuretemp(self, string, dir=1): - """ (deprecated) return temporary directory path with - the given string as the trailing part. It is usually - better to use the 'tmpdir' function argument which - provides an empty unique-per-test-invocation directory - and is guaranteed to be empty. - """ - # py.log._apiwarn(">1.1", "use tmpdir function argument") - from .deprecated import PYTEST_ENSURETEMP - - warnings.warn(PYTEST_ENSURETEMP, stacklevel=2) - return self.getbasetemp().ensure(string, dir=dir) - - def mktemp(self, basename, numbered=True): - """Create a subdirectory of the base temporary directory and return it. - If ``numbered``, ensure the directory is unique by adding a number - prefix greater than any existing one. - """ - return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) - - def getbasetemp(self): - """backward compat wrapper for ``_tmppath_factory.getbasetemp``""" - return py.path.local(self._tmppath_factory.getbasetemp().resolve()) - - -def get_user(): - """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 - - -def pytest_configure(config): - """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) - t = TempdirFactory(tmppath_handler) - config._cleanup.append(mp.undo) - mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) - mp.setattr(config, "_tmpdirhandler", t, raising=False) - mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) - - -@pytest.fixture(scope="session") -def tmpdir_factory(request): - """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. - """ - return request.config._tmpdirhandler - - -@pytest.fixture(scope="session") -def tmp_path_factory(request): - """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. - """ - return request.config._tmp_path_factory - - -def _mk_tmp(request, factory): - name = request.node.name - name = re.sub(r"[\W]", "_", name) - MAXVAL = 30 - name = name[:MAXVAL] - return factory.mktemp(name, numbered=True) - - -@pytest.fixture + + +@attr.s +class TempdirFactory(object): + """ + backward comptibility wrapper that implements + :class:``py.path.local`` for :class:``TempPathFactory`` + """ + + _tmppath_factory = attr.ib() + + def ensuretemp(self, string, dir=1): + """ (deprecated) return temporary directory path with + the given string as the trailing part. It is usually + better to use the 'tmpdir' function argument which + provides an empty unique-per-test-invocation directory + and is guaranteed to be empty. + """ + # py.log._apiwarn(">1.1", "use tmpdir function argument") + from .deprecated import PYTEST_ENSURETEMP + + warnings.warn(PYTEST_ENSURETEMP, stacklevel=2) + return self.getbasetemp().ensure(string, dir=dir) + + def mktemp(self, basename, numbered=True): + """Create a subdirectory of the base temporary directory and return it. + If ``numbered``, ensure the directory is unique by adding a number + prefix greater than any existing one. + """ + return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve()) + + def getbasetemp(self): + """backward compat wrapper for ``_tmppath_factory.getbasetemp``""" + return py.path.local(self._tmppath_factory.getbasetemp().resolve()) + + +def get_user(): + """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 + + +def pytest_configure(config): + """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) + t = TempdirFactory(tmppath_handler) + config._cleanup.append(mp.undo) + mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) + mp.setattr(config, "_tmpdirhandler", t, raising=False) + mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) + + +@pytest.fixture(scope="session") +def tmpdir_factory(request): + """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. + """ + return request.config._tmpdirhandler + + +@pytest.fixture(scope="session") +def tmp_path_factory(request): + """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. + """ + return request.config._tmp_path_factory + + +def _mk_tmp(request, factory): + name = request.node.name + name = re.sub(r"[\W]", "_", name) + MAXVAL = 30 + name = name[:MAXVAL] + return factory.mktemp(name, numbered=True) + + +@pytest.fixture def tmpdir(tmp_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. The returned object is a `py.path.local`_ - path object. - - .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html - """ + """Return a temporary directory path object + which is unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a `py.path.local`_ + path object. + + .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html + """ return py.path.local(tmp_path) - - -@pytest.fixture -def tmp_path(request, tmp_path_factory): - """Return a temporary directory path object - which is unique to each test function invocation, - created as a sub directory of the base temporary - directory. The returned object is a :class:`pathlib.Path` - object. - - .. note:: - - in python < 3.6 this is a pathlib2.Path - """ - - return _mk_tmp(request, tmp_path_factory) + + +@pytest.fixture +def tmp_path(request, tmp_path_factory): + """Return a temporary directory path object + which is unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a :class:`pathlib.Path` + object. + + .. note:: + + in python < 3.6 this is a pathlib2.Path + """ + + return _mk_tmp(request, tmp_path_factory) diff --git a/contrib/python/pytest/py2/_pytest/unittest.py b/contrib/python/pytest/py2/_pytest/unittest.py index a27b8e8945..3ff6f45d8d 100644 --- a/contrib/python/pytest/py2/_pytest/unittest.py +++ b/contrib/python/pytest/py2/_pytest/unittest.py @@ -1,69 +1,69 @@ # -*- coding: utf-8 -*- -""" discovery and running of std-library "unittest" style tests. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import sys -import traceback - -import _pytest._code +""" discovery and running of std-library "unittest" style tests. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys +import traceback + +import _pytest._code import pytest -from _pytest.compat import getimfunc -from _pytest.config import hookimpl -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 - - -def pytest_pycollect_makeitem(collector, name, obj): - # has unittest been imported and is obj a subclass of its TestCase? - try: - if not issubclass(obj, sys.modules["unittest"].TestCase): - return - except Exception: - return - # yes, so let's collect it - return UnitTestCase(name, parent=collector) - - -class UnitTestCase(Class): - # marker for fixturemanger.getfixtureinfo() - # to declare that our children do not support funcargs - nofuncargs = True - - def collect(self): - from unittest import TestLoader - - cls = self.obj - if not getattr(cls, "__test__", True): - return +from _pytest.compat import getimfunc +from _pytest.config import hookimpl +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 + + +def pytest_pycollect_makeitem(collector, name, obj): + # has unittest been imported and is obj a subclass of its TestCase? + try: + if not issubclass(obj, sys.modules["unittest"].TestCase): + return + except Exception: + return + # yes, so let's collect it + return UnitTestCase(name, parent=collector) + + +class UnitTestCase(Class): + # marker for fixturemanger.getfixtureinfo() + # to declare that our children do not support funcargs + nofuncargs = True + + def collect(self): + from unittest import TestLoader + + cls = self.obj + if not getattr(cls, "__test__", True): + return skipped = getattr(cls, "__unittest_skip__", False) 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) - yield TestCaseFunction(name, parent=self, 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) - if ut is None or runtest != ut.TestCase.runTest: - yield TestCaseFunction("runTest", parent=self) - + 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(name, parent=self, 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) + if ut is None or runtest != ut.TestCase.runTest: + yield TestCaseFunction("runTest", parent=self) + def _inject_setup_teardown_fixtures(self, cls): """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding teardown functions (#517)""" @@ -72,7 +72,7 @@ class UnitTestCase(Class): ) if class_fixture: cls.__pytest_class_setup = class_fixture - + method_fixture = _make_xunit_fixture( cls, "setup_method", "teardown_method", scope="function", pass_self=True ) @@ -106,186 +106,186 @@ def _make_xunit_fixture(obj, setup_name, teardown_name, scope, pass_self): return fixture -class TestCaseFunction(Function): - nofuncargs = True - _excinfo = None - _testcase = None - - def setup(self): - self._testcase = self.parent.obj(self.name) - self._fix_unittest_skip_decorator() - self._obj = getattr(self._testcase, self.name) - if hasattr(self, "_request"): - self._request._fillfixtures() - - def _fix_unittest_skip_decorator(self): - """ - The @unittest.skip decorator calls functools.wraps(self._testcase) - The call to functools.wraps() fails unless self._testcase - has a __name__ attribute. This is usually automatically supplied - if the test is a function or method, but we need to add manually - here. - - See issue #1169 - """ - if sys.version_info[0] == 2: - setattr(self._testcase, "__name__", self.name) - - def teardown(self): - self._testcase = None - self._obj = None - - def startTest(self, testcase): - pass - - def _addexcinfo(self, rawexcinfo): - # unwrap potential exception info (see twisted trial support below) - rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) - try: - excinfo = _pytest._code.ExceptionInfo(rawexcinfo) +class TestCaseFunction(Function): + nofuncargs = True + _excinfo = None + _testcase = None + + def setup(self): + self._testcase = self.parent.obj(self.name) + self._fix_unittest_skip_decorator() + self._obj = getattr(self._testcase, self.name) + if hasattr(self, "_request"): + self._request._fillfixtures() + + def _fix_unittest_skip_decorator(self): + """ + The @unittest.skip decorator calls functools.wraps(self._testcase) + The call to functools.wraps() fails unless self._testcase + has a __name__ attribute. This is usually automatically supplied + if the test is a function or method, but we need to add manually + here. + + See issue #1169 + """ + if sys.version_info[0] == 2: + setattr(self._testcase, "__name__", self.name) + + def teardown(self): + self._testcase = None + self._obj = None + + def startTest(self, testcase): + pass + + def _addexcinfo(self, rawexcinfo): + # unwrap potential exception info (see twisted trial support below) + rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) + try: + excinfo = _pytest._code.ExceptionInfo(rawexcinfo) # 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: # noqa - fail( - "ERROR: Unknown Incompatible Exception " - "representation:\n%r" % (rawexcinfo,), - pytrace=False, - ) - except KeyboardInterrupt: - raise - except fail.Exception: + 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: # noqa + 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) - - def addError(self, testcase, rawexcinfo): - self._addexcinfo(rawexcinfo) - - def addFailure(self, testcase, rawexcinfo): - self._addexcinfo(rawexcinfo) - - def addSkip(self, testcase, reason): - try: - skip(reason) - except skip.Exception: - self._skipped_by_mark = True - self._addexcinfo(sys.exc_info()) - - def addExpectedFailure(self, testcase, rawexcinfo, reason=""): - try: - xfail(str(reason)) - except xfail.Exception: - self._addexcinfo(sys.exc_info()) - - def addUnexpectedSuccess(self, testcase, reason=""): - self._unexpectedsuccess = reason - - def addSuccess(self, testcase): - pass - - def stopTest(self, testcase): - pass - - def _handle_skip(self): - # implements the skipping machinery (see #2137) - # analog to pythons Lib/unittest/case.py:run - testMethod = getattr(self._testcase, self._testcase._testMethodName) - if getattr(self._testcase.__class__, "__unittest_skip__", False) or getattr( - testMethod, "__unittest_skip__", False - ): - # If the class or method was skipped. - skip_why = getattr( - self._testcase.__class__, "__unittest_skip_why__", "" - ) or getattr(testMethod, "__unittest_skip_why__", "") - try: # PY3, unittest2 on PY2 - self._testcase._addSkip(self, self._testcase, skip_why) - except TypeError: # PY2 - if sys.version_info[0] != 2: - raise - self._testcase._addSkip(self, skip_why) - return True - return False - - def runtest(self): - if self.config.pluginmanager.get_plugin("pdbinvoke") is None: - self._testcase(result=self) - else: - # disables tearDown and cleanups for post mortem debugging (see #1890) - if self._handle_skip(): - return - self._testcase.debug() - - def _prunetraceback(self, excinfo): - 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, call): - if isinstance(item, TestCaseFunction): - if item._excinfo: - call.excinfo = item._excinfo.pop(0) - try: - del call.result - except AttributeError: - pass - - -# twisted trial support - - -@hookimpl(hookwrapper=True) -def pytest_runtest_protocol(item): - if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: - ut = 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 - - -def check_testcase_implements_trial_reporter(done=[]): - if done: - return - from zope.interface import classImplements - from twisted.trial.itrial import IReporter - - classImplements(TestCaseFunction, IReporter) - done.append(1) + self.__dict__.setdefault("_excinfo", []).append(excinfo) + + def addError(self, testcase, rawexcinfo): + self._addexcinfo(rawexcinfo) + + def addFailure(self, testcase, rawexcinfo): + self._addexcinfo(rawexcinfo) + + def addSkip(self, testcase, reason): + try: + skip(reason) + except skip.Exception: + self._skipped_by_mark = True + self._addexcinfo(sys.exc_info()) + + def addExpectedFailure(self, testcase, rawexcinfo, reason=""): + try: + xfail(str(reason)) + except xfail.Exception: + self._addexcinfo(sys.exc_info()) + + def addUnexpectedSuccess(self, testcase, reason=""): + self._unexpectedsuccess = reason + + def addSuccess(self, testcase): + pass + + def stopTest(self, testcase): + pass + + def _handle_skip(self): + # implements the skipping machinery (see #2137) + # analog to pythons Lib/unittest/case.py:run + testMethod = getattr(self._testcase, self._testcase._testMethodName) + if getattr(self._testcase.__class__, "__unittest_skip__", False) or getattr( + testMethod, "__unittest_skip__", False + ): + # If the class or method was skipped. + skip_why = getattr( + self._testcase.__class__, "__unittest_skip_why__", "" + ) or getattr(testMethod, "__unittest_skip_why__", "") + try: # PY3, unittest2 on PY2 + self._testcase._addSkip(self, self._testcase, skip_why) + except TypeError: # PY2 + if sys.version_info[0] != 2: + raise + self._testcase._addSkip(self, skip_why) + return True + return False + + def runtest(self): + if self.config.pluginmanager.get_plugin("pdbinvoke") is None: + self._testcase(result=self) + else: + # disables tearDown and cleanups for post mortem debugging (see #1890) + if self._handle_skip(): + return + self._testcase.debug() + + def _prunetraceback(self, excinfo): + 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, call): + if isinstance(item, TestCaseFunction): + if item._excinfo: + call.excinfo = item._excinfo.pop(0) + try: + del call.result + except AttributeError: + pass + + +# twisted trial support + + +@hookimpl(hookwrapper=True) +def pytest_runtest_protocol(item): + if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: + ut = 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 + + +def check_testcase_implements_trial_reporter(done=[]): + if done: + return + from zope.interface import classImplements + from twisted.trial.itrial import IReporter + + classImplements(TestCaseFunction, IReporter) + done.append(1) diff --git a/contrib/python/pytest/py2/_pytest/warning_types.py b/contrib/python/pytest/py2/_pytest/warning_types.py index 1af4c65eb4..861010a127 100644 --- a/contrib/python/pytest/py2/_pytest/warning_types.py +++ b/contrib/python/pytest/py2/_pytest/warning_types.py @@ -1,31 +1,31 @@ # -*- coding: utf-8 -*- -import attr - - -class PytestWarning(UserWarning): - """ - Bases: :class:`UserWarning`. - - Base class for all warnings emitted by pytest. - """ - - +import attr + + +class PytestWarning(UserWarning): + """ + Bases: :class:`UserWarning`. + + Base class for all warnings emitted by pytest. + """ + + class PytestAssertRewriteWarning(PytestWarning): - """ + """ Bases: :class:`PytestWarning`. - + Warning emitted by the pytest assert rewrite module. - """ - - + """ + + class PytestCacheWarning(PytestWarning): - """ + """ Bases: :class:`PytestWarning`. - + Warning emitted by the cache plugin in various situations. - """ - - + """ + + class PytestConfigWarning(PytestWarning): """ Bases: :class:`PytestWarning`. @@ -50,23 +50,23 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning): """ -class PytestExperimentalApiWarning(PytestWarning, FutureWarning): - """ - Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`. - - Warning category used to denote experiments in pytest. Use sparingly as the API might change or even be - removed completely in future version - """ - - @classmethod - def simple(cls, apiname): - return cls( - "{apiname} is an experimental api that may change over time".format( - apiname=apiname - ) - ) - - +class PytestExperimentalApiWarning(PytestWarning, FutureWarning): + """ + Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`. + + Warning category used to denote experiments in pytest. Use sparingly as the API might change or even be + removed completely in future version + """ + + @classmethod + def simple(cls, apiname): + return cls( + "{apiname} is an experimental api that may change over time".format( + apiname=apiname + ) + ) + + class PytestUnhandledCoroutineWarning(PytestWarning): """ Bases: :class:`PytestWarning`. @@ -94,19 +94,19 @@ class RemovedInPytest4Warning(PytestDeprecationWarning): """ -@attr.s -class UnformattedWarning(object): - """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. - - Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. - """ - - category = attr.ib() - template = attr.ib() - - def format(self, **kwargs): - """Returns an instance of the warning category, formatted with given kwargs""" - return self.category(self.template.format(**kwargs)) - - -PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") +@attr.s +class UnformattedWarning(object): + """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. + + Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. + """ + + category = attr.ib() + template = attr.ib() + + def format(self, **kwargs): + """Returns an instance of the warning category, formatted with given kwargs""" + return self.category(self.template.format(**kwargs)) + + +PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") diff --git a/contrib/python/pytest/py2/_pytest/warnings.py b/contrib/python/pytest/py2/_pytest/warnings.py index 4e93441205..a3debae462 100644 --- a/contrib/python/pytest/py2/_pytest/warnings.py +++ b/contrib/python/pytest/py2/_pytest/warnings.py @@ -1,180 +1,180 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import sys -import warnings -from contextlib import contextmanager - -import pytest -from _pytest import compat - -SHOW_PYTEST_WARNINGS_ARG = "-Walways::pytest.RemovedInPytest4Warning" - - -def _setoption(wmod, arg): - """ - Copy of the warning._setoption function but does not escape arguments. - """ - parts = arg.split(":") - if len(parts) > 5: - raise wmod._OptionError("too many fields (max 5): %r" % (arg,)) - while len(parts) < 5: - parts.append("") - action, message, category, module, lineno = [s.strip() for s in parts] - action = wmod._getaction(action) - category = wmod._getcategory(category) - if lineno: - try: - lineno = int(lineno) - if lineno < 0: - raise ValueError - except (ValueError, OverflowError): - raise wmod._OptionError("invalid lineno %r" % (lineno,)) - else: - lineno = 0 - wmod.filterwarnings(action, message, category, module, lineno) - - -def pytest_addoption(parser): - group = parser.getgroup("pytest-warnings") - group.addoption( - "-W", - "--pythonwarnings", - action="append", - help="set which warnings to report, see -W option of python itself.", - ) - parser.addini( - "filterwarnings", - type="linelist", - help="Each line specifies a pattern for " - "warnings.filterwarnings. " - "Processed after -W and --pythonwarnings.", - ) - - -def pytest_configure(config): - config.addinivalue_line( - "markers", - "filterwarnings(warning): add a warning filter to the given test. " - "see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings ", - ) - - -@contextmanager -def catch_warnings_for_item(config, ihook, when, item): - """ - 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. - - Each warning captured triggers the ``pytest_warning_captured`` hook. - """ - cmdline_filters = config.getoption("pythonwarnings") or [] - inifilters = config.getini("filterwarnings") - with warnings.catch_warnings(record=True) as log: - - 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("error", category=pytest.RemovedInPytest4Warning) - - # filters should have this precedence: mark, cmdline options, ini - # filters should be applied in the inverse order of precedence - for arg in inifilters: - _setoption(warnings, arg) - - for arg in cmdline_filters: - warnings._setoption(arg) - - if item is not None: - for mark in item.iter_markers(name="filterwarnings"): - for arg in mark.args: - _setoption(warnings, arg) - - yield - - for warning_message in log: - ihook.pytest_warning_captured.call_historic( - kwargs=dict(warning_message=warning_message, when=when, item=item) - ) - - -def warning_record_to_str(warning_message): +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys +import warnings +from contextlib import contextmanager + +import pytest +from _pytest import compat + +SHOW_PYTEST_WARNINGS_ARG = "-Walways::pytest.RemovedInPytest4Warning" + + +def _setoption(wmod, arg): + """ + Copy of the warning._setoption function but does not escape arguments. + """ + parts = arg.split(":") + if len(parts) > 5: + raise wmod._OptionError("too many fields (max 5): %r" % (arg,)) + while len(parts) < 5: + parts.append("") + action, message, category, module, lineno = [s.strip() for s in parts] + action = wmod._getaction(action) + category = wmod._getcategory(category) + if lineno: + try: + lineno = int(lineno) + if lineno < 0: + raise ValueError + except (ValueError, OverflowError): + raise wmod._OptionError("invalid lineno %r" % (lineno,)) + else: + lineno = 0 + wmod.filterwarnings(action, message, category, module, lineno) + + +def pytest_addoption(parser): + group = parser.getgroup("pytest-warnings") + group.addoption( + "-W", + "--pythonwarnings", + action="append", + help="set which warnings to report, see -W option of python itself.", + ) + parser.addini( + "filterwarnings", + type="linelist", + help="Each line specifies a pattern for " + "warnings.filterwarnings. " + "Processed after -W and --pythonwarnings.", + ) + + +def pytest_configure(config): + config.addinivalue_line( + "markers", + "filterwarnings(warning): add a warning filter to the given test. " + "see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings ", + ) + + +@contextmanager +def catch_warnings_for_item(config, ihook, when, item): + """ + 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. + + Each warning captured triggers the ``pytest_warning_captured`` hook. + """ + cmdline_filters = config.getoption("pythonwarnings") or [] + inifilters = config.getini("filterwarnings") + with warnings.catch_warnings(record=True) as log: + + 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("error", category=pytest.RemovedInPytest4Warning) + + # filters should have this precedence: mark, cmdline options, ini + # filters should be applied in the inverse order of precedence + for arg in inifilters: + _setoption(warnings, arg) + + for arg in cmdline_filters: + warnings._setoption(arg) + + if item is not None: + for mark in item.iter_markers(name="filterwarnings"): + for arg in mark.args: + _setoption(warnings, arg) + + yield + + for warning_message in log: + ihook.pytest_warning_captured.call_historic( + kwargs=dict(warning_message=warning_message, when=when, item=item) + ) + + +def warning_record_to_str(warning_message): """Convert a warnings.WarningMessage to a string. - + This takes lot of unicode shenaningans into account for Python 2. - When Python 2 support is dropped this function can be greatly simplified. - """ - warn_msg = warning_message.message - unicode_warning = False - if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): - new_args = [] - for m in warn_msg.args: - new_args.append( - compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m - ) - unicode_warning = list(warn_msg.args) != new_args - warn_msg.args = new_args - - msg = warnings.formatwarning( - warn_msg, - warning_message.category, - warning_message.filename, - warning_message.lineno, - warning_message.line, - ) - if unicode_warning: - warnings.warn( - "Warning is using unicode non convertible to ascii, " - "converting to a safe representation:\n {!r}".format(compat.safe_str(msg)), - UnicodeWarning, - ) - return msg - - -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_protocol(item): - 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): - 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): - config = terminalreporter.config - with catch_warnings_for_item( - config=config, ihook=config.hook, when="config", item=None - ): - yield - - + When Python 2 support is dropped this function can be greatly simplified. + """ + warn_msg = warning_message.message + unicode_warning = False + if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): + new_args = [] + for m in warn_msg.args: + new_args.append( + compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m + ) + unicode_warning = list(warn_msg.args) != new_args + warn_msg.args = new_args + + msg = warnings.formatwarning( + warn_msg, + warning_message.category, + warning_message.filename, + warning_message.lineno, + warning_message.line, + ) + if unicode_warning: + warnings.warn( + "Warning is using unicode non convertible to ascii, " + "converting to a safe representation:\n {!r}".format(compat.safe_str(msg)), + UnicodeWarning, + ) + return msg + + +@pytest.hookimpl(hookwrapper=True, tryfirst=True) +def pytest_runtest_protocol(item): + 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): + 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): + config = terminalreporter.config + with catch_warnings_for_item( + config=config, ihook=config.hook, when="config", item=None + ): + yield + + def _issue_warning_captured(warning, hook, stacklevel): - """ - This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: - at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured - hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. - - :param warning: the warning instance. + """ + This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: + at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured + hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. + + :param warning: the warning instance. :param hook: the hook caller - :param stacklevel: stacklevel forwarded to warnings.warn - """ - with warnings.catch_warnings(record=True) as records: - warnings.simplefilter("always", type(warning)) - warnings.warn(warning, stacklevel=stacklevel) + :param stacklevel: stacklevel forwarded to warnings.warn + """ + with warnings.catch_warnings(record=True) as records: + warnings.simplefilter("always", type(warning)) + warnings.warn(warning, stacklevel=stacklevel) hook.pytest_warning_captured.call_historic( - kwargs=dict(warning_message=records[0], when="config", item=None) - ) + kwargs=dict(warning_message=records[0], when="config", item=None) + ) diff --git a/contrib/python/pytest/py2/pytest.py b/contrib/python/pytest/py2/pytest.py index 4f1e440119..ccc77b476f 100644 --- a/contrib/python/pytest/py2/pytest.py +++ b/contrib/python/pytest/py2/pytest.py @@ -1,107 +1,107 @@ # -*- coding: utf-8 -*- -# PYTHON_ARGCOMPLETE_OK -""" -pytest: unit and functional testing with Python. -""" -# else we are imported -from _pytest import __version__ -from _pytest.assertion import register_assert_rewrite -from _pytest.config import cmdline -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 fillfixtures as _fillfuncargs -from _pytest.fixtures import fixture -from _pytest.fixtures import yield_fixture -from _pytest.freeze_support import freeze_includes -from _pytest.main import Session -from _pytest.mark import MARK_GEN as mark -from _pytest.mark import param -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.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 warns +# PYTHON_ARGCOMPLETE_OK +""" +pytest: unit and functional testing with Python. +""" +# else we are imported +from _pytest import __version__ +from _pytest.assertion import register_assert_rewrite +from _pytest.config import cmdline +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 fillfixtures as _fillfuncargs +from _pytest.fixtures import fixture +from _pytest.fixtures import yield_fixture +from _pytest.freeze_support import freeze_includes +from _pytest.main import Session +from _pytest.mark import MARK_GEN as mark +from _pytest.mark import param +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.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 warns 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 PytestUnknownMarkWarning -from _pytest.warning_types import PytestWarning -from _pytest.warning_types import RemovedInPytest4Warning - -set_trace = __pytestPDB.set_trace - -__all__ = [ - "__version__", - "_fillfuncargs", - "approx", - "Class", - "cmdline", - "Collector", - "deprecated_call", - "exit", - "fail", - "File", - "fixture", - "freeze_includes", - "Function", - "hookimpl", - "hookspec", - "importorskip", - "Instance", - "Item", - "main", - "mark", - "Module", - "Package", - "param", +from _pytest.warning_types import PytestWarning +from _pytest.warning_types import RemovedInPytest4Warning + +set_trace = __pytestPDB.set_trace + +__all__ = [ + "__version__", + "_fillfuncargs", + "approx", + "Class", + "cmdline", + "Collector", + "deprecated_call", + "exit", + "fail", + "File", + "fixture", + "freeze_includes", + "Function", + "hookimpl", + "hookspec", + "importorskip", + "Instance", + "Item", + "main", + "mark", + "Module", + "Package", + "param", "PytestAssertRewriteWarning", "PytestCacheWarning", "PytestCollectionWarning", "PytestConfigWarning", - "PytestDeprecationWarning", - "PytestExperimentalApiWarning", + "PytestDeprecationWarning", + "PytestExperimentalApiWarning", "PytestUnhandledCoroutineWarning", "PytestUnknownMarkWarning", - "PytestWarning", - "raises", - "register_assert_rewrite", - "RemovedInPytest4Warning", - "Session", - "set_trace", - "skip", - "UsageError", - "warns", - "xfail", - "yield_fixture", -] - -if __name__ == "__main__": - # if run as a script or by 'python -m pytest' - # we trigger the below "else" condition by the following import - import pytest - - raise SystemExit(pytest.main()) -else: - - from _pytest.compat import _setup_collect_fakemodule - - _setup_collect_fakemodule() + "PytestWarning", + "raises", + "register_assert_rewrite", + "RemovedInPytest4Warning", + "Session", + "set_trace", + "skip", + "UsageError", + "warns", + "xfail", + "yield_fixture", +] + +if __name__ == "__main__": + # if run as a script or by 'python -m pytest' + # we trigger the below "else" condition by the following import + import pytest + + raise SystemExit(pytest.main()) +else: + + from _pytest.compat import _setup_collect_fakemodule + + _setup_collect_fakemodule() diff --git a/contrib/python/pytest/py2/ya.make b/contrib/python/pytest/py2/ya.make index 2160ba1007..4a4214535c 100644 --- a/contrib/python/pytest/py2/ya.make +++ b/contrib/python/pytest/py2/ya.make @@ -1,27 +1,27 @@ # Generated by devtools/yamaker (pypi). - + PY2_LIBRARY() OWNER(dmitko g:python-contrib) - + VERSION(4.6.11) - + LICENSE(MIT) - -PEERDIR( - contrib/python/atomicwrites - contrib/python/attrs + +PEERDIR( + contrib/python/atomicwrites + contrib/python/attrs contrib/python/funcsigs contrib/python/importlib-metadata - contrib/python/more-itertools + contrib/python/more-itertools contrib/python/packaging contrib/python/pathlib2 - contrib/python/pluggy - contrib/python/py - contrib/python/six + contrib/python/pluggy + contrib/python/py + contrib/python/six contrib/python/wcwidth -) - +) + NO_LINT() NO_CHECK_IMPORTS( @@ -29,10 +29,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/_py2traceback.py _pytest/_code/code.py @@ -40,56 +40,56 @@ PY_SRCS( _pytest/_io/__init__.py _pytest/_io/saferepr.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/fixtures.py - _pytest/freeze_support.py - _pytest/helpconfig.py - _pytest/hookspec.py - _pytest/junitxml.py - _pytest/logging.py - _pytest/main.py + _pytest/config/argparsing.py + _pytest/config/exceptions.py + _pytest/config/findpaths.py + _pytest/debugging.py + _pytest/deprecated.py + _pytest/doctest.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/evaluate.py - _pytest/mark/legacy.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/evaluate.py + _pytest/mark/legacy.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/python.py - _pytest/python_api.py - _pytest/recwarn.py - _pytest/reports.py - _pytest/resultlog.py - _pytest/runner.py - _pytest/setuponly.py - _pytest/setupplan.py - _pytest/skipping.py - _pytest/stepwise.py - _pytest/terminal.py - _pytest/tmpdir.py - _pytest/unittest.py + _pytest/python_api.py + _pytest/recwarn.py + _pytest/reports.py + _pytest/resultlog.py + _pytest/runner.py + _pytest/setuponly.py + _pytest/setupplan.py + _pytest/skipping.py + _pytest/stepwise.py + _pytest/terminal.py + _pytest/tmpdir.py + _pytest/unittest.py _pytest/warning_types.py - _pytest/warnings.py - pytest.py -) - + _pytest/warnings.py + pytest.py +) + RESOURCE_FILES( PREFIX contrib/python/pytest/py2/ .dist-info/METADATA @@ -97,4 +97,4 @@ RESOURCE_FILES( .dist-info/top_level.txt ) -END() +END() 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() diff --git a/contrib/python/pytest/ya.make b/contrib/python/pytest/ya.make index 207cbd26c1..e4d27ea57a 100644 --- a/contrib/python/pytest/ya.make +++ b/contrib/python/pytest/ya.make @@ -1,15 +1,15 @@ -PY23_LIBRARY() - +PY23_LIBRARY() + LICENSE(Service-Py23-Proxy) - + OWNER(g:python-contrib) - -IF (PYTHON2) + +IF (PYTHON2) PEERDIR(contrib/python/pytest/py2) ELSE() PEERDIR(contrib/python/pytest/py3) -ENDIF() - +ENDIF() + NO_LINT() END() @@ -17,4 +17,4 @@ END() RECURSE( py2 py3 -) +) diff --git a/contrib/python/ya.make b/contrib/python/ya.make index f2ad3d75cb..d01ced9f3a 100644 --- a/contrib/python/ya.make +++ b/contrib/python/ya.make @@ -60,9 +60,9 @@ RECURSE( apsw aresponses argcomplete - argon2-cffi + argon2-cffi argon2-cffi-bindings - argparse-addons + argparse-addons arq arrow asciitree @@ -80,7 +80,7 @@ RECURSE( asyncssh asynctest asyncwhois - atomicwrites + atomicwrites atpublic AttrDict attrs @@ -107,7 +107,7 @@ RECURSE( betamax-serializers billiard binaryornot - bincopy + bincopy biplist bitarray black @@ -251,7 +251,7 @@ RECURSE( django-extensions django-fernet-fields django-filebrowser-no-grappelli - django-filter + django-filter django-fsm django-grappelli django-guardian @@ -278,7 +278,7 @@ RECURSE( django-partial-index django-pdb django-phonenumbers - django-picklefield + django-picklefield django-post-office django-postgrespool2 django-proxy-storage @@ -301,7 +301,7 @@ RECURSE( django-test-migrations django-timezone-field django-treebeard - django-waffle + django-waffle django-webpack-loader django-webtest django-whatever @@ -314,7 +314,7 @@ RECURSE( djangorestframework-xml dm.xmlsec.binding dnspython - docker + docker docopt docstring-parser docutils @@ -632,7 +632,7 @@ RECURSE( mongolock mongomock monotonic - more-itertools + more-itertools moto moto/standalone motor @@ -746,7 +746,7 @@ RECURSE( platformdirs plotly plotly/_plotly_utils - pluggy + pluggy plugincode plumbum ply @@ -818,7 +818,7 @@ RECURSE( pygit2 PyGithub Pygments - pygrib + pygrib pygtrie PyHamcrest pyjavaproperties @@ -844,7 +844,7 @@ RECURSE( PyPDF2 pyperclip PyPika - pyproj + pyproj pyre2 pyrepl pyresample @@ -1035,7 +1035,7 @@ RECURSE( starlette statsd statsmodels - stevedore + stevedore StrEnum structlog subprocess32 |