From 522d59e844abe815790acf733a0aa13b597acfac Mon Sep 17 00:00:00 2001 From: Dmitry Malinovsky Date: Sat, 10 Dec 2016 16:45:40 +0600 Subject: [PATCH 001/159] Use session.config.hook instead of ihook. Fixes #2124 --- _pytest/fixtures.py | 8 +++---- testing/python/fixture.py | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 28bcd4d8d..c2a27b83d 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -754,8 +754,8 @@ class FixtureDef: func = self._finalizer.pop() func() finally: - ihook = self._fixturemanager.session.ihook - ihook.pytest_fixture_post_finalizer(fixturedef=self) + hook = self._fixturemanager.session.config.hook + hook.pytest_fixture_post_finalizer(fixturedef=self) # even if finalization fails, we invalidate # the cached fixture value if hasattr(self, "cached_result"): @@ -783,8 +783,8 @@ class FixtureDef: self.finish() assert not hasattr(self, "cached_result") - ihook = self._fixturemanager.session.ihook - return ihook.pytest_fixture_setup(fixturedef=self, request=request) + hook = self._fixturemanager.session.config.hook + return hook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): return ("" % diff --git a/testing/python/fixture.py b/testing/python/fixture.py index be99ed833..3e84be138 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2984,4 +2984,49 @@ class TestParameterizedSubRequest: """.format(fixfile.strpath, testfile.basename)) +def test_pytest_fixture_setup_hook(testdir): + testdir.makeconftest(""" + import pytest + def pytest_fixture_setup(): + print('pytest_fixture_setup hook called') + """) + testdir.makepyfile(""" + import pytest + + @pytest.fixture() + def some(): + return 'some' + + def test_func(some): + assert some == 'some' + """) + result = testdir.runpytest("-s") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*pytest_fixture_setup hook called*", + ]) + + +def test_pytest_fixture_post_finalizer_hook(testdir): + testdir.makeconftest(""" + import pytest + + def pytest_fixture_post_finalizer(): + print('pytest_fixture_post_finalizer hook called') + """) + testdir.makepyfile(""" + import pytest + + @pytest.fixture() + def some(): + return 'some' + + def test_func(some): + assert some == 'some' + """) + result = testdir.runpytest("-s") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*pytest_fixture_post_finalizer hook called*", + ]) From a63b34c68596e4cddec6d899d8b27417ea4ce72c Mon Sep 17 00:00:00 2001 From: Dmitry Malinovsky Date: Tue, 20 Dec 2016 10:44:09 +0600 Subject: [PATCH 002/159] Switch to item fspath --- _pytest/fixtures.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index c2a27b83d..b951ae818 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -783,7 +783,9 @@ class FixtureDef: self.finish() assert not hasattr(self, "cached_result") - hook = self._fixturemanager.session.config.hook + hook = self._fixturemanager.session.gethookproxy( + request._pyfuncitem.fspath + ) return hook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): From c68a89b4a78a4bd4ab5ab1b139f9fe5b002bdad8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 31 Jul 2017 11:10:29 +0200 Subject: [PATCH 003/159] remove preinit, its no longer needed --- _pytest/config.py | 9 --------- changelog/2236.removal | 1 + pytest.py | 3 +-- 3 files changed, 2 insertions(+), 11 deletions(-) create mode 100644 changelog/2236.removal diff --git a/_pytest/config.py b/_pytest/config.py index d0ec62096..bdfe99b99 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -100,8 +100,6 @@ def directory_arg(path, optname): return path -_preinit = [] - default_plugins = ( "mark main terminal runner python fixtures debugging unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion " @@ -113,14 +111,7 @@ builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") -def _preloadplugins(): - assert not _preinit - _preinit.append(get_config()) - - def get_config(): - if _preinit: - return _preinit.pop(0) # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() config = Config(pluginmanager) diff --git a/changelog/2236.removal b/changelog/2236.removal new file mode 100644 index 000000000..013327291 --- /dev/null +++ b/changelog/2236.removal @@ -0,0 +1 @@ +- remove plugin preinit, we no longer need to do that because the namespace is initialized in the module now \ No newline at end of file diff --git a/pytest.py b/pytest.py index 1c914a6ed..2b681b64b 100644 --- a/pytest.py +++ b/pytest.py @@ -7,7 +7,7 @@ pytest: unit and functional testing with Python. # else we are imported from _pytest.config import ( - main, UsageError, _preloadplugins, cmdline, + main, UsageError, cmdline, hookspec, hookimpl ) from _pytest.fixtures import fixture, yield_fixture @@ -74,5 +74,4 @@ if __name__ == '__main__': else: from _pytest.compat import _setup_collect_fakemodule - _preloadplugins() # to populate pytest.* namespace so help(pytest) works _setup_collect_fakemodule() From e90f876b348cb070d4b1a50e1f7f8312c17ee1de Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 31 Jul 2017 13:48:25 +0200 Subject: [PATCH 004/159] remove the last own implementation of pytest_namespace --- _pytest/main.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index 4bddf1e2d..ee1ce00a2 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -83,15 +83,6 @@ def pytest_addoption(parser): help="base temporary directory for this test run.") -def pytest_namespace(): - """keeping this one works around a deeper startup issue in pytest - - i tried to find it for a while but the amount of time turned unsustainable, - so i put a hack in to revisit later - """ - return {} - - def pytest_configure(config): __import__('pytest').config = config # compatibiltiy From ceb016514b6b62cfe327db1cb5bdc3fef3244900 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 3 Jul 2017 15:56:11 +0200 Subject: [PATCH 005/159] remove dead code - Node._memoizedcall --- _pytest/main.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index 4bddf1e2d..f05fa74b0 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -363,24 +363,6 @@ class Node(object): def teardown(self): pass - def _memoizedcall(self, attrname, function): - exattrname = "_ex_" + attrname - failure = getattr(self, exattrname, None) - if failure is not None: - py.builtin._reraise(failure[0], failure[1], failure[2]) - if hasattr(self, attrname): - return getattr(self, attrname) - try: - res = function() - except py.builtin._sysex: - raise - except: - failure = sys.exc_info() - setattr(self, exattrname, failure) - raise - setattr(self, attrname, res) - return res - def listchain(self): """ return list of all parent collectors up to self, starting from root of collection tree. """ From 74d536314f3c7401181806a2b267c7012517fcf1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 18 Jul 2017 08:24:48 +0200 Subject: [PATCH 006/159] pytester: make pytest fullpath a constant --- _pytest/pytester.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 674adca94..0aa460bb1 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -22,6 +22,12 @@ from _pytest.main import Session, EXIT_OK from _pytest.assertion.rewrite import AssertionRewritingHook +PYTEST_FULLPATH = os.path.abspath( + pytest.__file__.rstrip("oc") + ).replace("$py.class", ".py") + + + def pytest_addoption(parser): # group = parser.getgroup("pytester", "pytester (self-tests) options") parser.addoption('--lsof', @@ -35,14 +41,6 @@ def pytest_addoption(parser): def pytest_configure(config): - # This might be called multiple times. Only take the first. - global _pytest_fullpath - try: - _pytest_fullpath - except NameError: - _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) - _pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py") - if config.getvalue("lsof"): checker = LsofFdLeakChecker() if checker.matching_platform(): @@ -971,7 +969,7 @@ class Testdir: def _getpytestargs(self): # we cannot use "(sys.executable,script)" # because on windows the script is e.g. a pytest.exe - return (sys.executable, _pytest_fullpath,) # noqa + return (sys.executable, PYTEST_FULLPATH) # noqa def runpython(self, script): """Run a python script using sys.executable as interpreter. From 8a2e6a8d51a6c50f807fb908578a11b3855b5fa1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 31 Jul 2017 16:49:30 -0300 Subject: [PATCH 007/159] Fix linting --- _pytest/pytester.py | 1 - 1 file changed, 1 deletion(-) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 0aa460bb1..2c558183b 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -27,7 +27,6 @@ PYTEST_FULLPATH = os.path.abspath( ).replace("$py.class", ".py") - def pytest_addoption(parser): # group = parser.getgroup("pytester", "pytester (self-tests) options") parser.addoption('--lsof', From dcaeef7c10d0df00100367a8c75813e761348c70 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 31 Jul 2017 22:21:09 +0200 Subject: [PATCH 008/159] take review comments into account --- changelog/2236.removal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2236.removal b/changelog/2236.removal index 013327291..84f98d009 100644 --- a/changelog/2236.removal +++ b/changelog/2236.removal @@ -1 +1 @@ -- remove plugin preinit, we no longer need to do that because the namespace is initialized in the module now \ No newline at end of file +- Remove internal ``_preloadplugins()`` function. This removal is part of the ``pytest_namespace()`` hook deprecation. \ No newline at end of file From dc563e4954403c5b9738e903ca224cc099b3f526 Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Wed, 2 Aug 2017 23:03:52 +0530 Subject: [PATCH 009/159] convert py module references to six module --- _pytest/_code/code.py | 4 +--- _pytest/_code/source.py | 3 ++- _pytest/assertion/__init__.py | 4 ++-- _pytest/assertion/rewrite.py | 23 ++++++++++++----------- _pytest/assertion/truncate.py | 6 +++--- _pytest/assertion/util.py | 7 ++++--- _pytest/capture.py | 9 ++++----- _pytest/config.py | 9 +++++---- _pytest/main.py | 3 ++- _pytest/monkeypatch.py | 9 ++++----- _pytest/nose.py | 3 +-- _pytest/pastebin.py | 4 ++-- _pytest/pytester.py | 11 ++++++----- _pytest/python.py | 7 ++++--- _pytest/runner.py | 2 +- _pytest/skipping.py | 4 ++-- _pytest/terminal.py | 7 ++++--- setup.py | 2 +- 18 files changed, 60 insertions(+), 57 deletions(-) diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py index 5750211f2..182de0468 100644 --- a/_pytest/_code/code.py +++ b/_pytest/_code/code.py @@ -8,8 +8,6 @@ from _pytest.compat import _PY2, _PY3, PY35, safe_str import py builtin_repr = repr -reprlib = py.builtin._tryimport('repr', 'reprlib') - if _PY3: from traceback import format_exception_only else: @@ -235,7 +233,7 @@ class TracebackEntry(object): except KeyError: return False - if py.builtin.callable(tbh): + if callable(tbh): return tbh(None if self._excinfo is None else self._excinfo()) else: return tbh diff --git a/_pytest/_code/source.py b/_pytest/_code/source.py index e21fecb1e..2959d635a 100644 --- a/_pytest/_code/source.py +++ b/_pytest/_code/source.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, generators, print_function from bisect import bisect_right import sys +import six import inspect import tokenize import py @@ -32,7 +33,7 @@ class Source(object): partlines = part.lines elif isinstance(part, (tuple, list)): partlines = [x.rstrip("\n") for x in part] - elif isinstance(part, py.builtin._basestring): + elif isinstance(part, six.string_types): partlines = part.split('\n') if rstrip: while partlines: diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index b0ef667d5..e9e39dae0 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -2,8 +2,8 @@ support for presenting detailed information in failing assertions. """ from __future__ import absolute_import, division, print_function -import py import sys +import six from _pytest.assertion import util from _pytest.assertion import rewrite @@ -126,7 +126,7 @@ def pytest_runtest_setup(item): if new_expl: new_expl = truncate.truncate_if_required(new_expl, item) new_expl = [line.replace("\n", "\\n") for line in new_expl] - res = py.builtin._totext("\n~").join(new_expl) + res = six.text_type("\n~").join(new_expl) if item.config.getvalue("assertmode") == "rewrite": res = res.replace("%", "%%") return res diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 992002b81..956ff487f 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -8,6 +8,7 @@ import imp import marshal import os import re +import six import struct import sys import types @@ -405,10 +406,10 @@ def _saferepr(obj): """ repr = py.io.saferepr(obj) - if py.builtin._istext(repr): - t = py.builtin.text + if isinstance(repr, six.text_type): + t = six.text_type else: - t = py.builtin.bytes + t = six.binary_type return repr.replace(t("\n"), t("\\n")) @@ -427,16 +428,16 @@ def _format_assertmsg(obj): # 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. - if py.builtin._istext(obj) or py.builtin._isbytes(obj): + if isinstance(obj, six.text_type) or isinstance(obj, six.binary_type): s = obj is_repr = False else: s = py.io.saferepr(obj) is_repr = True - if py.builtin._istext(s): - t = py.builtin.text + if isinstance(s, six.text_type): + t = six.text_type else: - t = py.builtin.bytes + t = six.binary_type s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%")) if is_repr: s = s.replace(t("\\n"), t("\n~")) @@ -444,15 +445,15 @@ def _format_assertmsg(obj): def _should_repr_global_name(obj): - return not hasattr(obj, "__name__") and not py.builtin.callable(obj) + return not hasattr(obj, "__name__") and not callable(obj) def _format_boolop(explanations, is_or): explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" - if py.builtin._istext(explanation): - t = py.builtin.text + if isinstance(explanation, six.text_type): + t = six.text_type else: - t = py.builtin.bytes + t = six.binary_type return explanation.replace(t('%'), t('%%')) diff --git a/_pytest/assertion/truncate.py b/_pytest/assertion/truncate.py index 1e1306356..2ed12e2e5 100644 --- a/_pytest/assertion/truncate.py +++ b/_pytest/assertion/truncate.py @@ -7,7 +7,7 @@ Current default behaviour is to truncate assertion explanations at from __future__ import absolute_import, division, print_function import os -import py +import six DEFAULT_MAX_LINES = 8 @@ -74,8 +74,8 @@ def _truncate_explanation(input_lines, max_lines=None, max_chars=None): msg += ' ({0} lines hidden)'.format(truncated_line_count) msg += ", {0}" .format(USAGE_MSG) truncated_explanation.extend([ - py.builtin._totext(""), - py.builtin._totext(msg), + six.text_type(""), + six.text_type(msg), ]) return truncated_explanation diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 41e66448d..69cd9c630 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -4,13 +4,14 @@ import pprint import _pytest._code import py +import six try: from collections import Sequence except ImportError: Sequence = list -u = py.builtin._totext +u = six.text_type # The _reprcompare attribute on the util module is used by the new assertion # interpretation code and assertion rewriter to detect this plugin was @@ -174,9 +175,9 @@ def _diff_text(left, right, verbose=False): """ from difflib import ndiff explanation = [] - if isinstance(left, py.builtin.bytes): + if isinstance(left, six.binary_type): left = u(repr(left)[1:-1]).replace(r'\n', '\n') - if isinstance(right, py.builtin.bytes): + if isinstance(right, six.binary_type): right = u(repr(right)[1:-1]).replace(r'\n', '\n') if not verbose: i = 0 # just in case left or right has zero length diff --git a/_pytest/capture.py b/_pytest/capture.py index a4171f0fa..1a5d8cf8d 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -11,11 +11,10 @@ import io from io import UnsupportedOperation from tempfile import TemporaryFile -import py +import six import pytest from _pytest.compat import CaptureIO -unicode = py.builtin.text patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'} @@ -246,7 +245,7 @@ class EncodedFile(object): self.encoding = encoding def write(self, obj): - if isinstance(obj, unicode): + if isinstance(obj, six.text_type): obj = obj.encode(self.encoding, "replace") self.buffer.write(obj) @@ -377,7 +376,7 @@ class FDCapture: if res: enc = getattr(f, "encoding", None) if enc and isinstance(res, bytes): - res = py.builtin._totext(res, enc, "replace") + res = six.text_type(res, enc, "replace") f.truncate(0) f.seek(0) return res @@ -402,7 +401,7 @@ class FDCapture: def writeorg(self, data): """ write to original file descriptor. """ - if py.builtin._istext(data): + if isinstance(data, six.text_type): data = data.encode("utf8") # XXX use encoding of original stream os.write(self.targetfd_save, data) diff --git a/_pytest/config.py b/_pytest/config.py index d0ec62096..93d9cc4a7 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -6,6 +6,7 @@ import traceback import types import warnings +import six import py # DON't import pytest here because it causes import cycle troubles import sys @@ -158,7 +159,7 @@ def _prepareconfig(args=None, plugins=None): try: if plugins: for plugin in plugins: - if isinstance(plugin, py.builtin._basestring): + if isinstance(plugin, six.string_types): pluginmanager.consider_pluginarg(plugin) else: pluginmanager.register(plugin) @@ -430,7 +431,7 @@ class PytestPluginManager(PluginManager): # "terminal" or "capture". Those plugins are registered under their # basename for historic purposes but must be imported with the # _pytest prefix. - assert isinstance(modname, (py.builtin.text, str)), "module name as text required, got %r" % modname + assert isinstance(modname, (six.text_type, str)), "module name as text required, got %r" % modname modname = str(modname) if self.get_plugin(modname) is not None: return @@ -643,7 +644,7 @@ class Argument: pass else: # this might raise a keyerror as well, don't want to catch that - if isinstance(typ, py.builtin._basestring): + if isinstance(typ, six.string_types): if typ == 'choice': warnings.warn( 'type argument to addoption() is a string %r.' @@ -956,7 +957,7 @@ class Config(object): ) res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) - if not py.builtin.any(res): + if not any(res): for line in str(excrepr).split("\n"): sys.stderr.write("INTERNALERROR> %s\n" % line) sys.stderr.flush() diff --git a/_pytest/main.py b/_pytest/main.py index 274b39782..21c53a8e7 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import functools import os +import six import sys import _pytest @@ -397,7 +398,7 @@ class Node(object): ``marker`` can be a string or pytest.mark.* instance. """ from _pytest.mark import MarkDecorator, MARK_GEN - if isinstance(marker, py.builtin._basestring): + 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") diff --git a/_pytest/monkeypatch.py b/_pytest/monkeypatch.py index 39ac77013..40ae560f0 100644 --- a/_pytest/monkeypatch.py +++ b/_pytest/monkeypatch.py @@ -4,8 +4,7 @@ from __future__ import absolute_import, division, print_function import os import sys import re - -from py.builtin import _basestring +import six from _pytest.fixtures import fixture RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") @@ -79,7 +78,7 @@ def annotated_getattr(obj, name, ann): def derive_importpath(import_path, raising): - if not isinstance(import_path, _basestring) or "." not in import_path: + 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) @@ -125,7 +124,7 @@ class MonkeyPatch: import inspect if value is notset: - if not isinstance(target, _basestring): + 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") @@ -155,7 +154,7 @@ class MonkeyPatch: """ __tracebackhide__ = True if name is notset: - if not isinstance(target, _basestring): + if not isinstance(target, six.string_types): raise TypeError("use delattr(target, name) or " "delattr(target) with target being a dotted " "import string") diff --git a/_pytest/nose.py b/_pytest/nose.py index d246c5603..c81542ead 100644 --- a/_pytest/nose.py +++ b/_pytest/nose.py @@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function import sys -import py from _pytest import unittest, runner, python from _pytest.config import hookimpl @@ -66,7 +65,7 @@ def is_potential_nosetest(item): def call_optional(obj, name): method = getattr(obj, name, None) isfixture = hasattr(method, "_pytestfixturefunction") - if method is not None and not isfixture and py.builtin.callable(method): + if method is not None and not isfixture and callable(method): # If there's any problems allow the exception to raise rather than # silently ignoring them method() diff --git a/_pytest/pastebin.py b/_pytest/pastebin.py index 9d689819f..b588b021b 100644 --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function import pytest +import six import sys import tempfile @@ -16,7 +17,6 @@ def pytest_addoption(parser): @pytest.hookimpl(trylast=True) def pytest_configure(config): - import py if config.option.pastebin == "all": tr = config.pluginmanager.getplugin('terminalreporter') # if no terminal reporter plugin is present, nothing we can do here; @@ -29,7 +29,7 @@ def pytest_configure(config): def tee_write(s, **kwargs): oldwrite(s, **kwargs) - if py.builtin._istext(s): + if isinstance(s, six.text_type): s = s.encode('utf-8') config._pastebinfile.write(s) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 674adca94..75fa687af 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -7,6 +7,7 @@ import os import platform import re import subprocess +import six import sys import time import traceback @@ -485,8 +486,8 @@ class Testdir: def _makefile(self, ext, args, kwargs, encoding="utf-8"): items = list(kwargs.items()) if args: - source = py.builtin._totext("\n").join( - map(py.builtin._totext, args)) + py.builtin._totext("\n") + source = six.text_type("\n").join( + map(six.text_type, args)) + six.text_type("\n") basename = self.request.function.__name__ items.insert(0, (basename, source)) ret = None @@ -496,12 +497,12 @@ class Testdir: source = Source(value) def my_totext(s, encoding="utf-8"): - if py.builtin._isbytes(s): - s = py.builtin._totext(s, encoding=encoding) + if isinstance(s, six.binary_type): + s = six.text_type(s, encoding=encoding) return s source_unicode = "\n".join([my_totext(line) for line in source.lines]) - source = py.builtin._totext(source_unicode) + source = six.text_type(source_unicode) content = source.strip().encode(encoding) # + "\n" # content = content.rstrip() + "\n" p.write(content, "wb") diff --git a/_pytest/python.py b/_pytest/python.py index 267372888..12e4d8608 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -10,6 +10,7 @@ from textwrap import dedent from itertools import count import py +import six from _pytest.mark import MarkerError from _pytest.config import hookimpl @@ -613,7 +614,7 @@ class Generator(FunctionMixin, PyCollector): if not isinstance(obj, (tuple, list)): obj = (obj,) # explicit naming - if isinstance(obj[0], py.builtin._basestring): + if isinstance(obj[0], six.string_types): name = obj[0] obj = obj[1:] else: @@ -725,7 +726,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): self.cls = cls self._calls = [] - self._ids = py.builtin.set() + self._ids = set() self._arg2fixturedefs = fixtureinfo.name2fixturedefs def parametrize(self, argnames, argvalues, indirect=False, ids=None, @@ -827,7 +828,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): raise ValueError('%d tests specified with %d ids' % ( len(parameters), len(ids))) for id_value in ids: - if id_value is not None and not isinstance(id_value, py.builtin._basestring): + if id_value is not None and not isinstance(id_value, six.string_types): msg = 'ids must be list of strings, found: %s (type: %s)' raise ValueError(msg % (saferepr(id_value), type(id_value).__name__)) ids = idmaker(argnames, parameters, idfn, ids, self.config) diff --git a/_pytest/runner.py b/_pytest/runner.py index b5829f46d..5fe56216f 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -430,7 +430,7 @@ class SetupState(object): is called at the end of teardown_all(). """ assert colitem and not isinstance(colitem, tuple) - assert py.builtin.callable(finalizer) + assert callable(finalizer) # assert colitem in self.stack # some unit tests don't setup stack :/ self._finalizers.setdefault(colitem, []).append(finalizer) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index b11aea801..c812cd4d3 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -2,10 +2,10 @@ from __future__ import absolute_import, division, print_function import os +import six import sys import traceback -import py from _pytest.config import hookimpl from _pytest.mark import MarkInfo, MarkDecorator from _pytest.runner import fail, skip @@ -133,7 +133,7 @@ class MarkEvaluator: args = (kwargs['condition'],) for expr in args: self.expr = expr - if isinstance(expr, py.builtin._basestring): + if isinstance(expr, six.string_types): d = self._getglobals() result = cached_eval(self.item.config, expr, d) else: diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 7dd10924a..0a023d1f3 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -9,6 +9,7 @@ from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED import pytest import py +import six import sys import time import platform @@ -174,8 +175,8 @@ class TerminalReporter: self._tw.write(content, **markup) def write_line(self, line, **markup): - if not py.builtin._istext(line): - line = py.builtin.text(line, errors="replace") + if not isinstance(line, six.text_type): + line = six.text_type(line, errors="replace") self.ensure_newline() self._tw.line(line, **markup) @@ -194,7 +195,7 @@ class TerminalReporter: self._tw.line(msg, **kw) def pytest_internalerror(self, excrepr): - for line in py.builtin.text(excrepr).split("\n"): + for line in six.text_type(excrepr).split("\n"): self.write_line("INTERNALERROR> " + line) return 1 diff --git a/setup.py b/setup.py index 751868c04..b7a05a86a 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def has_environment_marker_support(): def main(): - install_requires = ['py>=1.4.33', 'setuptools'] # pluggy is vendored in _pytest.vendored_packages + install_requires = ['py>=1.4.33', 'six>=1.10.0','setuptools'] # pluggy is vendored in _pytest.vendored_packages extras_require = {} if has_environment_marker_support(): extras_require[':python_version=="2.6"'] = ['argparse'] From 2e33d9b35ed1ac09bf06c63496ce1abb7b7cb651 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 4 Aug 2017 00:03:43 -0300 Subject: [PATCH 010/159] Add changelog entry for using six for portability --- changelog/2642.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2642.trivial diff --git a/changelog/2642.trivial b/changelog/2642.trivial new file mode 100644 index 000000000..bae449dba --- /dev/null +++ b/changelog/2642.trivial @@ -0,0 +1 @@ +Refactored internal Python 2/3 compatibility code to use ``six``. From eb462582afa3ca83b2c278bc52fc4094e65f6553 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 10 Aug 2017 09:05:22 +0200 Subject: [PATCH 011/159] fix #2675 - store marks correctly in callspecs --- _pytest/mark.py | 4 ---- _pytest/python.py | 22 ++++++++++++++-------- changelog/2672.removal | 2 ++ changelog/2675.removal | 1 + testing/python/metafunc.py | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 changelog/2672.removal create mode 100644 changelog/2675.removal diff --git a/_pytest/mark.py b/_pytest/mark.py index 74473a9d7..f76d7da3b 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -67,10 +67,6 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): return cls(argval, marks=newmarks, id=None) - @property - def deprecated_arg_dict(self): - return dict((mark.name, mark) for mark in self.marks) - class MarkerError(Exception): diff --git a/_pytest/python.py b/_pytest/python.py index bdf14d841..b79486267 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -645,14 +645,14 @@ class CallSpec2(object): self._globalid_args = set() self._globalparam = NOTSET self._arg2scopenum = {} # used for sorting parametrized resources - self.keywords = {} + self.marks = [] self.indices = {} def copy(self, metafunc): cs = CallSpec2(self.metafunc) cs.funcargs.update(self.funcargs) cs.params.update(self.params) - cs.keywords.update(self.keywords) + cs.marks.extend(self.marks) cs.indices.update(self.indices) cs._arg2scopenum.update(self._arg2scopenum) cs._idlist = list(self._idlist) @@ -677,8 +677,8 @@ class CallSpec2(object): def id(self): return "-".join(map(str, filter(None, self._idlist))) - def setmulti(self, valtypes, argnames, valset, id, keywords, scopenum, - param_index): + 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] @@ -686,7 +686,7 @@ class CallSpec2(object): self.indices[arg] = param_index self._arg2scopenum[arg] = scopenum self._idlist.append(id) - self.keywords.update(keywords) + self.marks.extend(marks) def setall(self, funcargs, id, param): for x in funcargs: @@ -842,8 +842,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): 'equal to the number of names ({1})'.format( param.values, argnames)) newcallspec = callspec.copy(self) - newcallspec.setmulti(valtypes, argnames, param.values, a_id, - param.deprecated_arg_dict, scopenum, param_index) + newcallspec.setmulti2(valtypes, argnames, param.values, a_id, + param.marks, scopenum, param_index) newcalls.append(newcallspec) self._calls = newcalls @@ -1115,7 +1115,13 @@ class Function(FunctionMixin, main.Item, fixtures.FuncargnamesCompatAttr): self.keywords.update(self.obj.__dict__) if callspec: self.callspec = callspec - self.keywords.update(callspec.keywords) + # 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 if keywords: self.keywords.update(keywords) diff --git a/changelog/2672.removal b/changelog/2672.removal new file mode 100644 index 000000000..e660c27fd --- /dev/null +++ b/changelog/2672.removal @@ -0,0 +1,2 @@ +Internally change ``CallSpec2`` to have a list of marks instead of a broken mapping of keywords. +This removes the keywords attribute of the internal ``CallSpec2`` class. \ No newline at end of file diff --git a/changelog/2675.removal b/changelog/2675.removal new file mode 100644 index 000000000..44f597892 --- /dev/null +++ b/changelog/2675.removal @@ -0,0 +1 @@ +remove ParameterSet.deprecated_arg_dict - its not a public api and the lack of the underscore was a naming error. \ No newline at end of file diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index a0025a15a..cf3bea7be 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -158,7 +158,7 @@ class TestMetafunc(object): pass metafunc = self.Metafunc(func) metafunc.parametrize("y", []) - assert 'skip' in metafunc._calls[0].keywords + assert 'skip' == metafunc._calls[0].marks[0].name def test_parametrize_with_userobjects(self): def func(x, y): From 98bf5fc9beab7891014659ec82cb0008fb012df3 Mon Sep 17 00:00:00 2001 From: prokaktus Date: Sat, 12 Aug 2017 16:50:54 +0300 Subject: [PATCH 012/159] Fold skipped tests with global pytestmark variable --- AUTHORS | 1 + _pytest/skipping.py | 17 ++++++++++++++--- changelog/2549.feature | 1 + testing/test_skipping.py | 22 ++++++++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 changelog/2549.feature diff --git a/AUTHORS b/AUTHORS index 84833c642..cc4aaa6ad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -117,6 +117,7 @@ Matt Bachmann Matt Duck Matt Williams Matthias Hafner +Maxim Filipenko mbyt Michael Aquilina Michael Birtwell diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 5f927a6b4..2b5d0dded 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -345,6 +345,12 @@ def folded_skips(skipped): for event in skipped: key = event.longrepr assert len(key) == 3, (event, key) + keywords = getattr(event, 'keywords', {}) + # folding reports with global pytestmark variable + # this is workaround, because for now we cannot identify the scope of a skip marker + # TODO: revisit after marks scope would be fixed + if event.when == 'setup' and 'skip' in keywords and 'pytestmark' not in keywords: + key = (key[0], None, key[2], ) d.setdefault(key, []).append(event) l = [] for key, events in d.items(): @@ -367,6 +373,11 @@ def show_skipped(terminalreporter, lines): for num, fspath, lineno, reason in fskips: if reason.startswith("Skipped: "): reason = reason[9:] - lines.append( - "SKIP [%d] %s:%d: %s" % - (num, fspath, lineno + 1, reason)) + if lineno is not None: + lines.append( + "SKIP [%d] %s:%d: %s" % + (num, fspath, lineno + 1, reason)) + else: + lines.append( + "SKIP [%d] %s: %s" % + (num, fspath, reason)) diff --git a/changelog/2549.feature b/changelog/2549.feature new file mode 100644 index 000000000..4866d990d --- /dev/null +++ b/changelog/2549.feature @@ -0,0 +1 @@ +Report only once tests with global ``pytestmark`` variable. \ No newline at end of file diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 6608ccadf..1fbb9ed0f 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -676,6 +676,7 @@ def test_skip_reasons_folding(): ev1.longrepr = longrepr ev2 = X() + ev2.when = "execute" ev2.longrepr = longrepr ev2.skipped = True @@ -713,6 +714,27 @@ def test_skipped_reasons_functional(testdir): assert result.ret == 0 +def test_skipped_folding(testdir): + testdir.makepyfile( + test_one=""" + import pytest + pytestmark = pytest.mark.skip("Folding") + def setup_function(func): + pass + def test_func(): + pass + class TestClass(object): + def test_method(self): + pass + """, + ) + result = testdir.runpytest('-rs') + result.stdout.fnmatch_lines([ + "*SKIP*2*test_one.py: Folding" + ]) + assert result.ret == 0 + + def test_reportchars(testdir): testdir.makepyfile(""" import pytest From 333a9ad7fa9062773784ab654f9f851127791785 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 24 Aug 2017 15:54:33 -0400 Subject: [PATCH 013/159] Stop vendoring pluggy Resolves #2716 --- _pytest/_pluggy.py | 11 +- _pytest/vendored_packages/README.md | 13 - _pytest/vendored_packages/__init__.py | 0 .../pluggy-0.4.0.dist-info/DESCRIPTION.rst | 11 - .../pluggy-0.4.0.dist-info/INSTALLER | 1 - .../pluggy-0.4.0.dist-info/LICENSE.txt | 22 - .../pluggy-0.4.0.dist-info/METADATA | 40 - .../pluggy-0.4.0.dist-info/RECORD | 9 - .../pluggy-0.4.0.dist-info/WHEEL | 6 - .../pluggy-0.4.0.dist-info/metadata.json | 1 - .../pluggy-0.4.0.dist-info/top_level.txt | 1 - _pytest/vendored_packages/pluggy.py | 802 ------------------ setup.py | 2 +- 13 files changed, 4 insertions(+), 915 deletions(-) delete mode 100644 _pytest/vendored_packages/README.md delete mode 100644 _pytest/vendored_packages/__init__.py delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/DESCRIPTION.rst delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/INSTALLER delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/LICENSE.txt delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/METADATA delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/RECORD delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/WHEEL delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/metadata.json delete mode 100644 _pytest/vendored_packages/pluggy-0.4.0.dist-info/top_level.txt delete mode 100644 _pytest/vendored_packages/pluggy.py diff --git a/_pytest/_pluggy.py b/_pytest/_pluggy.py index 6cc1d3d54..0cb4a4fb4 100644 --- a/_pytest/_pluggy.py +++ b/_pytest/_pluggy.py @@ -1,11 +1,6 @@ """ -imports symbols from vendored "pluggy" if available, otherwise -falls back to importing "pluggy" from the default namespace. +Import symbols from ``pluggy`` """ from __future__ import absolute_import, division, print_function -try: - from _pytest.vendored_packages.pluggy import * # noqa - from _pytest.vendored_packages.pluggy import __version__ # noqa -except ImportError: - from pluggy import * # noqa - from pluggy import __version__ # noqa +from pluggy import * +from pluggy import __version__ diff --git a/_pytest/vendored_packages/README.md b/_pytest/vendored_packages/README.md deleted file mode 100644 index b5fe6febb..000000000 --- a/_pytest/vendored_packages/README.md +++ /dev/null @@ -1,13 +0,0 @@ -This directory vendors the `pluggy` module. - -For a more detailed discussion for the reasons to vendoring this -package, please see [this issue](https://github.com/pytest-dev/pytest/issues/944). - -To update the current version, execute: - -``` -$ pip install -U pluggy== --no-compile --target=_pytest/vendored_packages -``` - -And commit the modified files. The `pluggy-.dist-info` directory -created by `pip` should be added as well. diff --git a/_pytest/vendored_packages/__init__.py b/_pytest/vendored_packages/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/DESCRIPTION.rst b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/DESCRIPTION.rst deleted file mode 100644 index da0e7a6ed..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/DESCRIPTION.rst +++ /dev/null @@ -1,11 +0,0 @@ - -Plugin registration and hook calling for Python -=============================================== - -This is the plugin manager as used by pytest but stripped -of pytest specific details. - -During the 0.x series this plugin does not have much documentation -except extensive docstrings in the pluggy.py module. - - diff --git a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/INSTALLER b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/INSTALLER deleted file mode 100644 index a1b589e38..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/LICENSE.txt b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/LICENSE.txt deleted file mode 100644 index 121017d08..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/LICENSE.txt +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42) - -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/_pytest/vendored_packages/pluggy-0.4.0.dist-info/METADATA b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/METADATA deleted file mode 100644 index bd88517c9..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/METADATA +++ /dev/null @@ -1,40 +0,0 @@ -Metadata-Version: 2.0 -Name: pluggy -Version: 0.4.0 -Summary: plugin and hook calling mechanisms for python -Home-page: https://github.com/pytest-dev/pluggy -Author: Holger Krekel -Author-email: holger at merlinux.eu -License: MIT license -Platform: unix -Platform: linux -Platform: osx -Platform: win32 -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: POSIX -Classifier: Operating System :: Microsoft :: Windows -Classifier: Operating System :: MacOS :: MacOS X -Classifier: Topic :: Software Development :: Testing -Classifier: Topic :: Software Development :: Libraries -Classifier: Topic :: Utilities -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 - - -Plugin registration and hook calling for Python -=============================================== - -This is the plugin manager as used by pytest but stripped -of pytest specific details. - -During the 0.x series this plugin does not have much documentation -except extensive docstrings in the pluggy.py module. - - diff --git a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/RECORD b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/RECORD deleted file mode 100644 index 3003a3bf2..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/RECORD +++ /dev/null @@ -1,9 +0,0 @@ -pluggy.py,sha256=u0oG9cv-oLOkNvEBlwnnu8pp1AyxpoERgUO00S3rvpQ,31543 -pluggy-0.4.0.dist-info/DESCRIPTION.rst,sha256=ltvjkFd40LW_xShthp6RRVM6OB_uACYDFR3kTpKw7o4,307 -pluggy-0.4.0.dist-info/LICENSE.txt,sha256=ruwhUOyV1HgE9F35JVL9BCZ9vMSALx369I4xq9rhpkM,1134 -pluggy-0.4.0.dist-info/METADATA,sha256=pe2hbsqKFaLHC6wAQPpFPn0KlpcPfLBe_BnS4O70bfk,1364 -pluggy-0.4.0.dist-info/RECORD,, -pluggy-0.4.0.dist-info/WHEEL,sha256=9Z5Xm-eel1bTS7e6ogYiKz0zmPEqDwIypurdHN1hR40,116 -pluggy-0.4.0.dist-info/metadata.json,sha256=T3go5L2qOa_-H-HpCZi3EoVKb8sZ3R-fOssbkWo2nvM,1119 -pluggy-0.4.0.dist-info/top_level.txt,sha256=xKSCRhai-v9MckvMuWqNz16c1tbsmOggoMSwTgcpYHE,7 -pluggy-0.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 diff --git a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/WHEEL b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/WHEEL deleted file mode 100644 index 8b6dd1b5a..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/WHEEL +++ /dev/null @@ -1,6 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.29.0) -Root-Is-Purelib: true -Tag: py2-none-any -Tag: py3-none-any - diff --git a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/metadata.json b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/metadata.json deleted file mode 100644 index cde22aff0..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries", "Topic :: Utilities", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "extensions": {"python.details": {"contacts": [{"email": "holger at merlinux.eu", "name": "Holger Krekel", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}, "project_urls": {"Home": "https://github.com/pytest-dev/pluggy"}}}, "generator": "bdist_wheel (0.29.0)", "license": "MIT license", "metadata_version": "2.0", "name": "pluggy", "platform": "unix", "summary": "plugin and hook calling mechanisms for python", "version": "0.4.0"} \ No newline at end of file diff --git a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/top_level.txt b/_pytest/vendored_packages/pluggy-0.4.0.dist-info/top_level.txt deleted file mode 100644 index 11bdb5c1f..000000000 --- a/_pytest/vendored_packages/pluggy-0.4.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -pluggy diff --git a/_pytest/vendored_packages/pluggy.py b/_pytest/vendored_packages/pluggy.py deleted file mode 100644 index aebddad01..000000000 --- a/_pytest/vendored_packages/pluggy.py +++ /dev/null @@ -1,802 +0,0 @@ -""" -PluginManager, basic initialization and tracing. - -pluggy is the cristallized core of plugin management as used -by some 150 plugins for pytest. - -Pluggy uses semantic versioning. Breaking changes are only foreseen for -Major releases (incremented X in "X.Y.Z"). If you want to use pluggy in -your project you should thus use a dependency restriction like -"pluggy>=0.1.0,<1.0" to avoid surprises. - -pluggy is concerned with hook specification, hook implementations and hook -calling. For any given hook specification a hook call invokes up to N implementations. -A hook implementation can influence its position and type of execution: -if attributed "tryfirst" or "trylast" it will be tried to execute -first or last. However, if attributed "hookwrapper" an implementation -can wrap all calls to non-hookwrapper implementations. A hookwrapper -can thus execute some code ahead and after the execution of other hooks. - -Hook specification is done by way of a regular python function where -both the function name and the names of all its arguments are significant. -Each hook implementation function is verified against the original specification -function, including the names of all its arguments. To allow for hook specifications -to evolve over the livetime of a project, hook implementations can -accept less arguments. One can thus add new arguments and semantics to -a hook specification by adding another argument typically without breaking -existing hook implementations. - -The chosen approach is meant to let a hook designer think carefuly about -which objects are needed by an extension writer. By contrast, subclass-based -extension mechanisms often expose a lot more state and behaviour than needed, -thus restricting future developments. - -Pluggy currently consists of functionality for: - -- a way to register new hook specifications. Without a hook - specification no hook calling can be performed. - -- a registry of plugins which contain hook implementation functions. It - is possible to register plugins for which a hook specification is not yet - known and validate all hooks when the system is in a more referentially - consistent state. Setting an "optionalhook" attribution to a hook - implementation will avoid PluginValidationError's if a specification - is missing. This allows to have optional integration between plugins. - -- a "hook" relay object from which you can launch 1:N calls to - registered hook implementation functions - -- a mechanism for ordering hook implementation functions - -- mechanisms for two different type of 1:N calls: "firstresult" for when - the call should stop when the first implementation returns a non-None result. - And the other (default) way of guaranteeing that all hook implementations - will be called and their non-None result collected. - -- mechanisms for "historic" extension points such that all newly - registered functions will receive all hook calls that happened - before their registration. - -- a mechanism for discovering plugin objects which are based on - setuptools based entry points. - -- a simple tracing mechanism, including tracing of plugin calls and - their arguments. - -""" -import sys -import inspect - -__version__ = '0.4.0' - -__all__ = ["PluginManager", "PluginValidationError", "HookCallError", - "HookspecMarker", "HookimplMarker"] - -_py3 = sys.version_info > (3, 0) - - -class HookspecMarker: - """ Decorator helper class for marking functions as hook specifications. - - You can instantiate it with a project_name to get a decorator. - Calling PluginManager.add_hookspecs later will discover all marked functions - if the 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): - """ if passed a function, directly sets attributes on the function - which will make it discoverable to add_hookspecs(). If passed no - function, returns a decorator which can be applied to a function - 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 - 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)) - return func - - if function is not None: - return setattr_hookspec_opts(function) - else: - return setattr_hookspec_opts - - -class HookimplMarker: - """ Decorator helper class for marking functions as hook implementations. - - You can instantiate with a project_name to get a decorator. - Calling PluginManager.register later will discover all marked functions - if the 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 - which will make it discoverable to 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). - - If tryfirst is True this hook implementation will run as early as possible - in the chain of N hook implementations for a specfication. - - If trylast is True this hook implementation will run as late as possible - 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 an ``_CallOutcome`` 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) - - -class _TagTracer: - def __init__(self): - self._tag2proc = {} - self.writer = None - 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)) - return lines - - def processmessage(self, tags, args): - if self.writer is not None and args: - lines = self.format_message(tags, args) - self.writer(''.join(lines)) - try: - self._tag2proc[tags](tags, args) - except KeyError: - pass - - 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) - self._tag2proc[tags] = processor - - -class _TagTracerSub: - def __init__(self, root, tags): - self.root = root - self.tags = tags - - def __call__(self, *args): - self.root.processmessage(self.tags, args) - - def setmyprocessor(self, processor): - self.root.setprocessor(self.tags, processor) - - def get(self, name): - return self.__class__(self.root, self.tags + (name,)) - - -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)) - - -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 _CallOutcome 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 = _CallOutcome(func) - try: - wrap_controller.send(call_outcome) - _raise_wrapfail(wrap_controller, "has second yield") - except StopIteration: - pass - return call_outcome.get_result() - - -class _CallOutcome: - """ Outcome of a function call, either an exception or a proper result. - Calling the ``get_result`` method will return the result or reraise - the exception raised when the function was called. """ - excinfo = None - - def __init__(self, func): - try: - self.result = func() - except BaseException: - self.excinfo = sys.exc_info() - - def force_result(self, result): - self.result = result - self.excinfo = None - - def get_result(self): - if self.excinfo is None: - return self.result - else: - ex = self.excinfo - if _py3: - raise ex[1].with_traceback(ex[2]) - _reraise(*ex) # noqa - -if not _py3: - exec(""" -def _reraise(cls, val, tb): - raise cls, val, tb -""") - - -class _TracedHookExecution: - def __init__(self, pluginmanager, before, after): - self.pluginmanager = pluginmanager - self.before = before - self.after = after - self.oldcall = pluginmanager._inner_hookexec - assert not isinstance(self.oldcall, _TracedHookExecution) - self.pluginmanager._inner_hookexec = self - - def __call__(self, hook, hook_impls, kwargs): - self.before(hook.name, hook_impls, kwargs) - outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs)) - self.after(outcome, hook.name, hook_impls, kwargs) - return outcome.get_result() - - def undo(self): - self.pluginmanager._inner_hookexec = self.oldcall - - -class PluginManager(object): - """ Core Pluginmanager class which manages registration - of plugin objects and 1:N hook calling. - - You can register new hooks by calling ``add_hookspec(module_or_class)``. - You can register plugin objects (which contain hooks) by calling - ``register(plugin)``. The Pluginmanager is initialized with a - prefix that is searched for in the names of the dict of registered - plugin objects. An optional excludefunc allows to blacklist names which - are not considered as hooks despite a matching prefix. - - For debugging purposes you can call ``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 - will be recognized if their name matches the implprefix. """ - self.project_name = project_name - self._name2plugin = {} - self._plugin2hookcallers = {} - self._plugin_distinfo = [] - self.trace = _TagTracer().get("pluginmanage") - self.hook = _HookRelay(self.trace.root.get("hook")) - self._implprefix = implprefix - self._inner_hookexec = lambda hook, methods, kwargs: \ - _MultiCall(methods, kwargs, hook.spec_opts).execute() - - 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 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) - 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 - elif res is None and self._implprefix and name.startswith(self._implprefix): - 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 name blogs registering plugins of that name. """ - 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._wrappers + hc._nonwrappers): - 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 - by the caller of register(plugin, name). To obtain the name - of an registered plugin use ``get_name(plugin)`` instead.""" - 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 True if a plugin with the given name is registered. """ - 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( - "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" % - (hookimpl.plugin_name, hook.name)) - - for arg in hookimpl.argnames: - if arg not in hook.argnames: - raise PluginValidationError( - "Plugin %r\nhook %r\nargument %r not available\n" - "plugin definition: %s\n" - "available hookargs: %s" % - (hookimpl.plugin_name, hook.name, arg, - _formatdef(hookimpl.function), ", ".join(hook.argnames))) - - def check_pending(self): - """ Verify that all hooks which have not been verified against - a hook specification are optional, otherwise raise PluginValidationError""" - for name in self.hook.__dict__: - if name[0] != "_": - hook = getattr(self.hook, name) - if not hook.has_spec(): - for hookimpl in (hook._wrappers + hook._nonwrappers): - if not hookimpl.optionalhook: - raise PluginValidationError( - "unknown hook %r in plugin %r" % - (name, hookimpl.plugin)) - - def load_setuptools_entrypoints(self, entrypoint_name): - """ Load modules from querying the specified setuptools entrypoint name. - Return the number of loaded plugins. """ - from pkg_resources import (iter_entry_points, DistributionNotFound, - VersionConflict) - for ep in iter_entry_points(entrypoint_name): - # is the plugin registered or blocked? - if self.get_plugin(ep.name) or self.is_blocked(ep.name): - continue - try: - plugin = ep.load() - except DistributionNotFound: - continue - except VersionConflict as e: - raise PluginValidationError( - "Plugin %r could not be loaded: %s!" % (ep.name, e)) - self.register(plugin, name=ep.name) - self._plugin_distinfo.append((plugin, ep.dist)) - return len(self._plugin_distinfo) - - 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:`_CallOutcome <_pytest.vendored_packages.pluggy._CallOutcome>` object - which represents the result of the overall hook call. - """ - return _TracedHookExecution(self, before, after).undo - - def enable_tracing(self): - """ enable tracing of hook calls and return an undo function. """ - hooktrace = self.hook._trace - - 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.result) - hooktrace.root.indent -= 1 - - return self.add_hookcall_monitoring(before, after) - - def subset_hook_caller(self, name, remove_plugins): - """ Return a new _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._specmodule_or_class, - orig.spec_opts) - for hookimpl in (orig._wrappers + orig._nonwrappers): - 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 - - -class _MultiCall: - """ 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, specopts={}): - self.hook_impls = hook_impls - self.kwargs = kwargs - self.kwargs["__multicall__"] = self - self.specopts = specopts - - def execute(self): - all_kwargs = self.kwargs - self.results = results = [] - firstresult = self.specopts.get("firstresult") - - while self.hook_impls: - hook_impl = self.hook_impls.pop() - try: - args = [all_kwargs[argname] for argname in hook_impl.argnames] - except KeyError: - for argname in hook_impl.argnames: - if argname not in all_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.kwargs) - - -def varnames(func, startindex=None): - """ return argument name tuple 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 unless you are passing - an unbound method with Python3 (which has no supports for unbound methods) - """ - cache = getattr(func, "__dict__", {}) - try: - return cache["_varnames"] - except KeyError: - pass - if inspect.isclass(func): - try: - func = func.__init__ - except AttributeError: - return () - startindex = 1 - else: - if not inspect.isfunction(func) and not inspect.ismethod(func): - try: - func = getattr(func, '__call__', func) - except Exception: - return () - if startindex is None: - startindex = int(inspect.ismethod(func)) - - try: - rawcode = func.__code__ - except AttributeError: - return () - try: - x = rawcode.co_varnames[startindex:rawcode.co_argcount] - except AttributeError: - x = () - else: - defaults = func.__defaults__ - if defaults: - x = x[:-len(defaults)] - try: - cache["_varnames"] = x - except TypeError: - pass - return x - - -class _HookRelay: - """ hook holder object for performing 1:N hook calls where N is the number - of registered plugins. - - """ - - def __init__(self, trace): - self._trace = trace - - -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 - 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 hasattr(self, "_specmodule_or_class") - - def set_specification(self, specmodule_or_class, spec_opts): - assert not self.has_spec() - self._specmodule_or_class = specmodule_or_class - specfunc = getattr(specmodule_or_class, self.name) - argnames = varnames(specfunc, startindex=inspect.isclass(specmodule_or_class)) - assert "self" not in argnames # sanity check - self.argnames = ["__multicall__"] + list(argnames) - self.spec_opts = 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 _add_hookimpl(self, hookimpl): - 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) - - def __repr__(self): - return "<_HookCaller %r>" % (self.name,) - - def __call__(self, **kwargs): - assert not self.is_historic() - return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) - - def call_historic(self, proc=None, kwargs=None): - self._call_history.append((kwargs or {}, proc)) - # historizing hooks don't return results - self._hookexec(self, self._nonwrappers + self._wrappers, kwargs) - - 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, "", method, opts) - self._add_hookimpl(hookimpl) - try: - return self(**kwargs) - finally: - self._nonwrappers, self._wrappers = old - - def _maybe_apply_history(self, method): - if self.is_historic(): - for kwargs, proc in self._call_history: - res = self._hookexec(self, [method], kwargs) - if res and proc is not None: - proc(res[0]) - - -class HookImpl: - def __init__(self, plugin, plugin_name, function, hook_impl_opts): - self.function = function - self.argnames = varnames(self.function) - self.plugin = plugin - self.opts = hook_impl_opts - self.plugin_name = plugin_name - self.__dict__.update(hook_impl_opts) - - -class PluginValidationError(Exception): - """ plugin failed validation. """ - - -class HookCallError(Exception): - """ Hook was called wrongly. """ - - -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/setup.py b/setup.py index 792be6b41..e7fb194d2 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def has_environment_marker_support(): def main(): - install_requires = ['py>=1.4.33', 'six>=1.10.0','setuptools'] # pluggy is vendored in _pytest.vendored_packages + install_requires = ['py>=1.4.33', 'six>=1.10.0','setuptools', 'pluggy==0.4.0'] extras_require = {} if has_environment_marker_support(): extras_require[':python_version=="2.6"'] = ['argparse', 'ordereddict'] From cb700208e8aea49f94266f3bd08a38e5dcc88950 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 24 Aug 2017 16:48:54 -0400 Subject: [PATCH 014/159] Drop vendoring task --- tasks/vendoring.py | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 tasks/vendoring.py diff --git a/tasks/vendoring.py b/tasks/vendoring.py deleted file mode 100644 index 867f2946b..000000000 --- a/tasks/vendoring.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import absolute_import, print_function -import py -import invoke - -VENDOR_TARGET = py.path.local("_pytest/vendored_packages") -GOOD_FILES = 'README.md', '__init__.py' - -@invoke.task() -def remove_libs(ctx): - print("removing vendored libs") - for path in VENDOR_TARGET.listdir(): - if path.basename not in GOOD_FILES: - print(" ", path) - path.remove() - -@invoke.task(pre=[remove_libs]) -def update_libs(ctx): - print("installing libs") - ctx.run("pip install -t {target} pluggy".format(target=VENDOR_TARGET)) - ctx.run("git add {target}".format(target=VENDOR_TARGET)) - print("Please commit to finish the update after running the tests:") - print() - print(' git commit -am "Updated vendored libs"') From 756db2131f6009f0fee2b20e82484f3db1188f47 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 24 Aug 2017 16:53:24 -0400 Subject: [PATCH 015/159] Drop vendoring from packaging --- .coveragerc | 1 - setup.py | 2 +- tox.ini | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index 27db64e09..48670b41d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,4 +4,3 @@ omit = *standalonetemplate.py # oldinterpret could be removed, as it is no longer used in py26+ *oldinterpret.py - vendored_packages diff --git a/setup.py b/setup.py index e7fb194d2..5698bed81 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def main(): setup_requires=['setuptools-scm'], install_requires=install_requires, extras_require=extras_require, - packages=['_pytest', '_pytest.assertion', '_pytest._code', '_pytest.vendored_packages'], + packages=['_pytest', '_pytest.assertion', '_pytest._code'], py_modules=['pytest'], zip_safe=False, ) diff --git a/tox.ini b/tox.ini index 61a9ece8d..907b7891b 100644 --- a/tox.ini +++ b/tox.ini @@ -162,7 +162,7 @@ usedevelop = True deps = autopep8 commands = - autopep8 --in-place -r --max-line-length=120 --exclude=vendored_packages,test_source_multiline_block.py _pytest testing + autopep8 --in-place -r --max-line-length=120 --exclude=test_source_multiline_block.py _pytest testing [testenv:jython] changedir = testing @@ -213,4 +213,3 @@ filterwarnings = [flake8] max-line-length = 120 -exclude = _pytest/vendored_packages/pluggy.py From 9ab83083d162fcde13d60b1e1709fbbd14449a4b Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 24 Aug 2017 16:53:37 -0400 Subject: [PATCH 016/159] Update docs --- _pytest/config.py | 4 ++-- doc/en/writing_plugins.rst | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index dedffd5a6..2fcbe9a09 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -165,7 +165,7 @@ def _prepareconfig(args=None, plugins=None): class PytestPluginManager(PluginManager): """ - Overwrites :py:class:`pluggy.PluginManager <_pytest.vendored_packages.pluggy.PluginManager>` to add pytest-specific + Overwrites :py:class:`pluggy.PluginManager <_pytest._pluggy.PluginManager>` to add pytest-specific functionality: * loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and @@ -203,7 +203,7 @@ class PytestPluginManager(PluginManager): """ .. deprecated:: 2.8 - Use :py:meth:`pluggy.PluginManager.add_hookspecs <_pytest.vendored_packages.pluggy.PluginManager.add_hookspecs>` + Use :py:meth:`pluggy.PluginManager.add_hookspecs <_pytest._pluggy.PluginManager.add_hookspecs>` instead. """ warning = dict(code="I2", diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index d5ad73b4b..6e96fe7c4 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -454,7 +454,7 @@ hook wrappers and passes the same arguments as to the regular hooks. At the yield point of the hook wrapper pytest will execute the next hook implementations and return their result to the yield point in the form of -a :py:class:`CallOutcome <_pytest.vendored_packages.pluggy._CallOutcome>` instance which encapsulates a result or +a :py:class:`CallOutcome <_pytest._pluggy._CallOutcome>` instance which encapsulates a result or exception info. The yield point itself will thus typically not raise exceptions (unless there are bugs). @@ -519,7 +519,7 @@ Here is the order of execution: Plugin1). 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield - point. The yield receives a :py:class:`CallOutcome <_pytest.vendored_packages.pluggy._CallOutcome>` instance which encapsulates + point. The yield receives a :py:class:`CallOutcome <_pytest._pluggy._CallOutcome>` instance which encapsulates the result from calling the non-wrappers. Wrappers shall not modify the result. It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with @@ -716,7 +716,7 @@ Reference of objects involved in hooks :members: :inherited-members: -.. autoclass:: _pytest.vendored_packages.pluggy._CallOutcome() +.. autoclass:: _pytest._pluggy._CallOutcome() :members: .. autofunction:: _pytest.config.get_plugin_manager() @@ -726,7 +726,7 @@ Reference of objects involved in hooks :undoc-members: :show-inheritance: -.. autoclass:: _pytest.vendored_packages.pluggy.PluginManager() +.. autoclass:: _pytest._pluggy.PluginManager() :members: .. currentmodule:: _pytest.pytester From ff35c17ecf61053727a3f453d9d37d2d5969f41d Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 25 Aug 2017 11:46:55 -0400 Subject: [PATCH 017/159] Drop wrapper module; import directly --- _pytest/_pluggy.py | 6 ------ _pytest/config.py | 6 +++--- _pytest/hookspec.py | 2 +- _pytest/python.py | 2 +- _pytest/terminal.py | 3 +-- doc/en/writing_plugins.rst | 8 ++++---- testing/test_terminal.py | 2 +- 7 files changed, 11 insertions(+), 18 deletions(-) delete mode 100644 _pytest/_pluggy.py diff --git a/_pytest/_pluggy.py b/_pytest/_pluggy.py deleted file mode 100644 index 0cb4a4fb4..000000000 --- a/_pytest/_pluggy.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Import symbols from ``pluggy`` -""" -from __future__ import absolute_import, division, print_function -from pluggy import * -from pluggy import __version__ diff --git a/_pytest/config.py b/_pytest/config.py index 2fcbe9a09..fd295fc73 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -14,7 +14,7 @@ import os import _pytest._code import _pytest.hookspec # the extension point definitions import _pytest.assertion -from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker +from pluggy import PluginManager, HookimplMarker, HookspecMarker from _pytest.compat import safe_str hookimpl = HookimplMarker("pytest") @@ -165,7 +165,7 @@ def _prepareconfig(args=None, plugins=None): class PytestPluginManager(PluginManager): """ - Overwrites :py:class:`pluggy.PluginManager <_pytest._pluggy.PluginManager>` to add pytest-specific + Overwrites :py:class:`pluggy.PluginManager ` to add pytest-specific functionality: * loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and @@ -203,7 +203,7 @@ class PytestPluginManager(PluginManager): """ .. deprecated:: 2.8 - Use :py:meth:`pluggy.PluginManager.add_hookspecs <_pytest._pluggy.PluginManager.add_hookspecs>` + Use :py:meth:`pluggy.PluginManager.add_hookspecs ` instead. """ warning = dict(code="I2", diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index e5c966e58..93cf91b78 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -1,6 +1,6 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ -from _pytest._pluggy import HookspecMarker +from pluggy import HookspecMarker hookspec = HookspecMarker("pytest") diff --git a/_pytest/python.py b/_pytest/python.py index b79486267..e3d79b2e9 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -15,7 +15,7 @@ from _pytest.mark import MarkerError from _pytest.config import hookimpl import _pytest -import _pytest._pluggy as pluggy +import pluggy from _pytest import fixtures from _pytest import main from _pytest.compat import ( diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 0a023d1f3..f7304f1e7 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -13,8 +13,7 @@ import six import sys import time import platform - -import _pytest._pluggy as pluggy +import pluggy def pytest_addoption(parser): diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 6e96fe7c4..5f151b4bb 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -454,7 +454,7 @@ hook wrappers and passes the same arguments as to the regular hooks. At the yield point of the hook wrapper pytest will execute the next hook implementations and return their result to the yield point in the form of -a :py:class:`CallOutcome <_pytest._pluggy._CallOutcome>` instance which encapsulates a result or +a :py:class:`CallOutcome ` instance which encapsulates a result or exception info. The yield point itself will thus typically not raise exceptions (unless there are bugs). @@ -519,7 +519,7 @@ Here is the order of execution: Plugin1). 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield - point. The yield receives a :py:class:`CallOutcome <_pytest._pluggy._CallOutcome>` instance which encapsulates + point. The yield receives a :py:class:`CallOutcome ` instance which encapsulates the result from calling the non-wrappers. Wrappers shall not modify the result. It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with @@ -716,7 +716,7 @@ Reference of objects involved in hooks :members: :inherited-members: -.. autoclass:: _pytest._pluggy._CallOutcome() +.. autoclass:: pluggy._CallOutcome() :members: .. autofunction:: _pytest.config.get_plugin_manager() @@ -726,7 +726,7 @@ Reference of objects involved in hooks :undoc-members: :show-inheritance: -.. autoclass:: _pytest._pluggy.PluginManager() +.. autoclass:: pluggy.PluginManager() :members: .. currentmodule:: _pytest.pytester diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 6b20c3a48..9b03f4ce7 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -5,7 +5,7 @@ from __future__ import absolute_import, division, print_function import collections import sys -import _pytest._pluggy as pluggy +import pluggy import _pytest._code import py import pytest From fe415e3ff89f1514e3ed21b2cd9cc7d7dc9caceb Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 25 Aug 2017 11:49:02 -0400 Subject: [PATCH 018/159] Use latest patch release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5698bed81..7b781ed69 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def has_environment_marker_support(): def main(): - install_requires = ['py>=1.4.33', 'six>=1.10.0','setuptools', 'pluggy==0.4.0'] + install_requires = ['py>=1.4.33', 'six>=1.10.0','setuptools', 'pluggy>=0.4.0,<0.5'] extras_require = {} if has_environment_marker_support(): extras_require[':python_version=="2.6"'] = ['argparse', 'ordereddict'] From 312891daa692e0daa456769b4a9307aba067600c Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 25 Aug 2017 15:34:42 -0400 Subject: [PATCH 019/159] Add a trivial changelog entry --- changelog/2719.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2719.trivial diff --git a/changelog/2719.trivial b/changelog/2719.trivial new file mode 100644 index 000000000..008f1dd20 --- /dev/null +++ b/changelog/2719.trivial @@ -0,0 +1 @@ +Stop vendoring ``pluggy`` - we're missing out on it's latest changes for not much benefit From 78a027e128d8846a2babdffee5c38c374e5c93d9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 30 Aug 2017 16:39:44 +0200 Subject: [PATCH 020/159] simplyfy ascii escaping by using backslashreplace error handling --- _pytest/compat.py | 9 +-------- changelog/2734.trivial | 1 + 2 files changed, 2 insertions(+), 8 deletions(-) create mode 100644 changelog/2734.trivial diff --git a/_pytest/compat.py b/_pytest/compat.py index 45f9f86d4..edb68c075 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -121,7 +121,6 @@ if sys.version_info[:2] == (2, 6): if _PY3: - import codecs imap = map izip = zip STRING_TYPES = bytes, str @@ -146,13 +145,7 @@ if _PY3: """ if isinstance(val, bytes): - 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 '' + return val.decode('ascii', 'backslashreplace') else: return val.encode('unicode_escape').decode('ascii') else: diff --git a/changelog/2734.trivial b/changelog/2734.trivial new file mode 100644 index 000000000..bbf701d16 --- /dev/null +++ b/changelog/2734.trivial @@ -0,0 +1 @@ +- simplify ascii string escaping by using the backslashreplace error handler \ No newline at end of file From 221797c609d9d032c48df0b7e4d0b4f062eb0c0e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 31 Aug 2017 17:40:05 -0300 Subject: [PATCH 021/159] Encode utf-8 byte strings in pytester's makefile Fix #2738 --- _pytest/pytester.py | 27 +++++++++++---------------- changelog/2738.bugfix | 1 + testing/test_pytester.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 changelog/2738.bugfix diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 2d2683574..fc9b8d9cb 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -480,29 +480,24 @@ class Testdir: if not hasattr(self, '_olddir'): self._olddir = old - def _makefile(self, ext, args, kwargs, encoding="utf-8"): + 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 = six.text_type("\n").join( - map(six.text_type, args)) + six.text_type("\n") + source = u"\n".join(to_text(x) for x in args) basename = self.request.function.__name__ items.insert(0, (basename, source)) + ret = None - for name, value in items: - p = self.tmpdir.join(name).new(ext=ext) + for basename, value in items: + p = self.tmpdir.join(basename).new(ext=ext) p.dirpath().ensure_dir() source = Source(value) - - def my_totext(s, encoding="utf-8"): - if isinstance(s, six.binary_type): - s = six.text_type(s, encoding=encoding) - return s - - source_unicode = "\n".join([my_totext(line) for line in source.lines]) - source = six.text_type(source_unicode) - content = source.strip().encode(encoding) # + "\n" - # content = content.rstrip() + "\n" - p.write(content, "wb") + 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 diff --git a/changelog/2738.bugfix b/changelog/2738.bugfix new file mode 100644 index 000000000..c53869f49 --- /dev/null +++ b/changelog/2738.bugfix @@ -0,0 +1 @@ +Internal ``pytester`` plugin properly encodes ``bytes`` arguments to ``utf-8``. diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 0e8669698..9508c2954 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function import pytest import os @@ -120,6 +121,16 @@ def test_makepyfile_unicode(testdir): testdir.makepyfile(unichr(0xfffd)) +def test_makepyfile_utf8(testdir): + """Ensure makepyfile accepts utf-8 bytes as input (#2738)""" + utf8_contents = u""" + def setup_function(function): + mixed_encoding = u'São Paulo' + """.encode('utf-8') + p = testdir.makepyfile(utf8_contents) + assert u"mixed_encoding = u'São Paulo'".encode('utf-8') in p.read('rb') + + def test_inline_run_clean_modules(testdir): test_mod = testdir.makepyfile("def test_foo(): assert True") result = testdir.inline_run(str(test_mod)) From 11ec6aeafb6f07047c043055f4beb33d83f1d4ba Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 Sep 2017 18:33:30 -0300 Subject: [PATCH 022/159] Add test environment using pluggy from master branch Fix #2737 --- .travis.yml | 2 ++ appveyor.yml | 2 ++ setup.py | 6 +++++- tox.ini | 7 +++++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d8d58328..387ff7160 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,12 @@ env: - TOXENV=py27-xdist - TOXENV=py27-trial - TOXENV=py27-numpy + - TOXENV=py27-pluggymaster - TOXENV=py35-pexpect - TOXENV=py35-xdist - TOXENV=py35-trial - TOXENV=py35-numpy + - TOXENV=py35-pluggymaster - TOXENV=py27-nobyte - TOXENV=doctesting - TOXENV=freeze diff --git a/appveyor.yml b/appveyor.yml index abf033b4c..337d89aec 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,10 +21,12 @@ environment: - TOXENV: "py27-xdist" - TOXENV: "py27-trial" - TOXENV: "py27-numpy" + - TOXENV: "py27-pluggymaster" - TOXENV: "py35-pexpect" - TOXENV: "py35-xdist" - TOXENV: "py35-trial" - TOXENV: "py35-numpy" + - TOXENV: "py35-pluggymaster" - TOXENV: "py27-nobyte" - TOXENV: "doctesting" - TOXENV: "freeze" diff --git a/setup.py b/setup.py index 7b781ed69..4d74e6bca 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,11 @@ def has_environment_marker_support(): def main(): - install_requires = ['py>=1.4.33', 'six>=1.10.0','setuptools', 'pluggy>=0.4.0,<0.5'] + install_requires = ['py>=1.4.33', 'six>=1.10.0', 'setuptools'] + # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; + # used by tox.ini to test with pluggy master + if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ: + install_requires.append('pluggy>=0.4.0,<0.5') extras_require = {} if has_environment_marker_support(): extras_require[':python_version=="2.6"'] = ['argparse', 'ordereddict'] diff --git a/tox.ini b/tox.ini index 907b7891b..c72bd7c0a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 2.0 distshare = {homedir}/.tox/distshare -# make sure to update environment list on appveyor.yml +# make sure to update environment list in travis.yml and appveyor.yml envlist = linting py26 @@ -12,7 +12,7 @@ envlist = py36 py37 pypy - {py27,py35}-{pexpect,xdist,trial,numpy} + {py27,py35}-{pexpect,xdist,trial,numpy,pluggymaster} py27-nobyte doctesting freeze @@ -21,11 +21,14 @@ envlist = [testenv] commands = pytest --lsof -rfsxX {posargs:testing} passenv = USER USERNAME +setenv= + pluggymaster: _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 deps = hypothesis>=3.5.2 nose mock requests + pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master [testenv:py26] commands = pytest --lsof -rfsxX {posargs:testing} From 3dc0da9339e4952cb276b32f451fa8cdc23d38be Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 30 Aug 2017 20:23:55 -0300 Subject: [PATCH 023/159] Remove __multicall__ warning and usages in testing pluggy>=0.5 already warns about those --- _pytest/config.py | 11 ----------- testing/python/collect.py | 8 +++++--- testing/test_collection.py | 8 +++++--- testing/test_pluginmanager.py | 17 ----------------- testing/test_runner.py | 12 +++++++----- testing/test_unittest.py | 6 ++++-- 6 files changed, 21 insertions(+), 41 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index fd295fc73..795056449 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -241,17 +241,6 @@ class PytestPluginManager(PluginManager): "historic": hasattr(method, "historic")} return opts - def _verify_hook(self, hook, hookmethod): - super(PytestPluginManager, self)._verify_hook(hook, hookmethod) - if "__multicall__" in hookmethod.argnames: - fslineno = _pytest._code.getfslineno(hookmethod.function) - warning = dict(code="I1", - fslocation=fslineno, - nodeid=None, - message="%r hook uses deprecated __multicall__ " - "argument" % (hook.name)) - self._warn(warning) - def register(self, plugin, name=None): ret = super(PytestPluginManager, self).register(plugin, name) if ret: diff --git a/testing/python/collect.py b/testing/python/collect.py index bd7013b44..d67437392 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -809,10 +809,12 @@ class TestConftestCustomization(object): def test_customized_pymakemodule_issue205_subdir(self, testdir): b = testdir.mkdir("a").mkdir("b") b.join("conftest.py").write(_pytest._code.Source(""" - def pytest_pycollect_makemodule(__multicall__): - mod = __multicall__.execute() + import pytest + @pytest.hookimpl(hookwrapper=True) + def pytest_pycollect_makemodule(): + outcome = yield + mod = outcome.get_result() mod.obj.hello = "world" - return mod """)) b.join("test_module.py").write(_pytest._code.Source(""" def test_hello(): diff --git a/testing/test_collection.py b/testing/test_collection.py index 5d1654410..1fc1a5d89 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -276,10 +276,12 @@ class TestPrunetraceback(object): """) testdir.makeconftest(""" import pytest - def pytest_make_collect_report(__multicall__): - rep = __multicall__.execute() + @pytest.hookimpl(hookwrapper=True) + def pytest_make_collect_report(): + outcome = yield + rep = outcome.get_result() rep.headerlines += ["header1"] - return rep + outcome.set_result(rep) """) result = testdir.runpytest(p) result.stdout.fnmatch_lines([ diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index be7980c26..2838f83c5 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -155,23 +155,6 @@ class TestPytestPluginInteractions(object): ihook_b = session.gethookproxy(testdir.tmpdir.join('tests')) assert ihook_a is not ihook_b - def test_warn_on_deprecated_multicall(self, pytestpm): - warnings = [] - - class get_warnings(object): - def pytest_logwarning(self, message): - warnings.append(message) - - class Plugin(object): - def pytest_configure(self, __multicall__): - pass - - pytestpm.register(get_warnings()) - before = list(warnings) - pytestpm.register(Plugin()) - assert len(warnings) == len(before) + 1 - assert "deprecated" in warnings[-1] - def test_warn_on_deprecated_addhooks(self, pytestpm): warnings = [] diff --git a/testing/test_runner.py b/testing/test_runner.py index 1ab449ba3..1b39a989f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -637,12 +637,14 @@ def test_pytest_cmdline_main(testdir): def test_unicode_in_longrepr(testdir): testdir.makeconftest(""" - import py - def pytest_runtest_makereport(__multicall__): - rep = __multicall__.execute() + # -*- coding: utf-8 -*- + import pytest + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_makereport(): + outcome = yield + rep = outcome.get_result() if rep.when == "call": - rep.longrepr = py.builtin._totext("\\xc3\\xa4", "utf8") - return rep + rep.longrepr = u'ä' """) testdir.makepyfile(""" def test_out(): diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 84f432a54..8051deda4 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -770,8 +770,10 @@ def test_no_teardown_if_setupclass_failed(testdir): def test_issue333_result_clearing(testdir): testdir.makeconftest(""" - def pytest_runtest_call(__multicall__, item): - __multicall__.execute() + import pytest + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_call(item): + yield assert 0 """) testdir.makepyfile(""" From c42d966a4088d9ef26eac70040fe68029b71877c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 Sep 2017 18:55:21 -0300 Subject: [PATCH 024/159] Change all pytest report options to the more concise '-ra' in tox.ini --- tox.ini | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index c72bd7c0a..89383e6fb 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ envlist = docs [testenv] -commands = pytest --lsof -rfsxX {posargs:testing} +commands = pytest --lsof -ra {posargs:testing} passenv = USER USERNAME setenv= pluggymaster: _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 @@ -31,7 +31,7 @@ deps = pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master [testenv:py26] -commands = pytest --lsof -rfsxX {posargs:testing} +commands = pytest --lsof -ra {posargs:testing} # pinning mock to last supported version for python 2.6 deps = hypothesis<3.0 @@ -46,7 +46,7 @@ deps = mock nose commands = - pytest -n3 -rfsxX --runpytest=subprocess {posargs:testing} + pytest -n3 -ra --runpytest=subprocess {posargs:testing} [testenv:linting] @@ -69,26 +69,26 @@ deps = nose hypothesis>=3.5.2 commands = - pytest -n1 -rfsxX {posargs:testing} + pytest -n1 -ra {posargs:testing} [testenv:py35-xdist] deps = {[testenv:py27-xdist]deps} commands = - pytest -n3 -rfsxX {posargs:testing} + pytest -n3 -ra {posargs:testing} [testenv:py27-pexpect] changedir = testing platform = linux|darwin deps = pexpect commands = - pytest -rfsxX test_pdb.py test_terminal.py test_unittest.py + pytest -ra test_pdb.py test_terminal.py test_unittest.py [testenv:py35-pexpect] changedir = testing platform = linux|darwin deps = {[testenv:py27-pexpect]deps} commands = - pytest -rfsxX test_pdb.py test_terminal.py test_unittest.py + pytest -ra test_pdb.py test_terminal.py test_unittest.py [testenv:py27-nobyte] deps = @@ -98,7 +98,7 @@ distribute = true setenv = PYTHONDONTWRITEBYTECODE=1 commands = - pytest -n3 -rfsxX {posargs:testing} + pytest -n3 -ra {posargs:testing} [testenv:py27-trial] deps = twisted @@ -113,12 +113,12 @@ commands = [testenv:py27-numpy] deps=numpy commands= - pytest -rfsxX {posargs:testing/python/approx.py} + pytest -ra {posargs:testing/python/approx.py} [testenv:py35-numpy] deps=numpy commands= - pytest -rfsxX {posargs:testing/python/approx.py} + pytest -ra {posargs:testing/python/approx.py} [testenv:docs] skipsdist = True @@ -141,7 +141,7 @@ changedir = doc/ deps = PyYAML commands = - pytest -rfsxX en + pytest -ra en pytest --doctest-modules --pyargs _pytest [testenv:regen] @@ -170,7 +170,7 @@ commands = [testenv:jython] changedir = testing commands = - {envpython} {envbindir}/py.test-jython -rfsxX {posargs} + {envpython} {envbindir}/py.test-jython -ra {posargs} [testenv:freeze] changedir = testing/freeze @@ -197,7 +197,7 @@ commands = minversion = 2.0 plugins = pytester #--pyargs --doctest-modules --ignore=.tox -addopts = -rxsX -p pytester --ignore=testing/cx_freeze +addopts = -ra -p pytester --ignore=testing/cx_freeze rsyncdirs = tox.ini pytest.py _pytest testing python_files = test_*.py *_test.py testing/*/*.py python_classes = Test Acceptance From 9bbf14d0f6d68a175a440daffad97c63c51fde37 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 1 Sep 2017 20:40:14 -0300 Subject: [PATCH 025/159] Create explicit 'pluggymaster' env definitions For some reason, the previous approach brakes 'coveralls' because pip still tries to install the 'pluggy' master requirement (git+https://...) --- tox.ini | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 89383e6fb..59a151895 100644 --- a/tox.ini +++ b/tox.ini @@ -21,14 +21,11 @@ envlist = [testenv] commands = pytest --lsof -ra {posargs:testing} passenv = USER USERNAME -setenv= - pluggymaster: _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 deps = hypothesis>=3.5.2 nose mock requests - pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master [testenv:py26] commands = pytest --lsof -ra {posargs:testing} @@ -120,6 +117,24 @@ deps=numpy commands= pytest -ra {posargs:testing/python/approx.py} +[testenv:py27-pluggymaster] +passenv={[testenv]passenv} +commands={[testenv]commands} +setenv= + _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 +deps = + {[testenv]deps} + git+https://github.com/pytest-dev/pluggy.git@master + +[testenv:py35-pluggymaster] +passenv={[testenv]passenv} +commands={[testenv]commands} +setenv= + _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 +deps = + {[testenv]deps} + git+https://github.com/pytest-dev/pluggy.git@master + [testenv:docs] skipsdist = True usedevelop = True From d8ecca5ebd929f1be49033f8a06eed97494af3da Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Tue, 22 Aug 2017 13:48:29 +0200 Subject: [PATCH 026/159] Add test to design warns signature in TDD mimicking raises signature --- testing/test_recwarn.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 6895b1140..e4e7934cc 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -284,3 +284,25 @@ class TestWarns(object): ''') result = testdir.runpytest() result.stdout.fnmatch_lines(['*2 passed in*']) + + def test_match_regex(self): + with pytest.warns(UserWarning, match=r'must be \d+$'): + warnings.warn("value must be 42", UserWarning) + + with pytest.raises(AssertionError, match='pattern not found'): + with pytest.warns(UserWarning, match=r'must be \d+$'): + warnings.warn("this is not here", UserWarning) + + def test_one_from_multiple_warns(): + with warns(UserWarning, match=r'aaa'): + warnings.warn("cccccccccc", UserWarning) + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("aaaaaaaaaa", UserWarning) + + def test_none_of_multiple_warns(): + a, b, c = ('aaa', 'bbbbbbbbbb', 'cccccccccc') + expected_msg = "'{}' pattern not found in \['{}', '{}'\]".format(a, b, c) + with raises(AssertionError, match=expected_msg): + with warns(UserWarning, match=r'aaa'): + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("cccccccccc", UserWarning) From 13eac944ae860a3cc4dcde49a5a7dca814941670 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 4 Sep 2017 21:25:46 +0200 Subject: [PATCH 027/159] restore ascii escaping for python 3.3/3.4 --- _pytest/compat.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/_pytest/compat.py b/_pytest/compat.py index edb68c075..e93cf6c2a 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -7,6 +7,7 @@ import inspect import types import re import functools +import codecs import py @@ -126,6 +127,19 @@ if _PY3: STRING_TYPES = bytes, str UNICODE_TYPES = str, + 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: @@ -145,7 +159,7 @@ if _PY3: """ if isinstance(val, bytes): - return val.decode('ascii', 'backslashreplace') + return _bytes_to_ascii(val) else: return val.encode('unicode_escape').decode('ascii') else: From 8d1903fed386e5d2060c8a61339f0dd9d4df3b8f Mon Sep 17 00:00:00 2001 From: Tarcisio Fischer Date: Tue, 5 Sep 2017 15:22:04 -0300 Subject: [PATCH 028/159] Avoid creating arbitrary filenames for tmpdir on Testdir's constructor Fixes #2751 --- _pytest/pytester.py | 10 +--------- testing/test_collection.py | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index fc9b8d9cb..573640014 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -412,16 +412,8 @@ class Testdir: def __init__(self, request, tmpdir_factory): self.request = request self._mod_collections = WeakKeyDictionary() - # XXX remove duplication with tmpdir plugin - basetmp = tmpdir_factory.ensuretemp("testdir") name = request.function.__name__ - for i in range(100): - try: - tmpdir = basetmp.mkdir(name + str(i)) - except py.error.EEXIST: - continue - break - self.tmpdir = tmpdir + self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) self.plugins = [] self._savesyspath = (list(sys.path), list(sys.meta_path)) self._savemodulekeys = set(sys.modules) diff --git a/testing/test_collection.py b/testing/test_collection.py index 5d1654410..ab0c93bae 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -569,7 +569,6 @@ class Test_getinitialnodes(object): col = testdir.getnode(config, x) assert isinstance(col, pytest.Module) assert col.name == 'x.py' - assert col.parent.name == testdir.tmpdir.basename assert col.parent.parent is None for col in col.listchain(): assert col.config is config From de6b41e318a69d723c2e43aa6253991cd9921d5b Mon Sep 17 00:00:00 2001 From: Tarcisio Fischer Date: Tue, 5 Sep 2017 14:33:06 -0300 Subject: [PATCH 029/159] Update changelog and AUTHORS files, following the CONTRIBUTING guidelines --- AUTHORS | 1 + changelog/2751.bugfix | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/2751.bugfix diff --git a/AUTHORS b/AUTHORS index cc4aaa6ad..717c19b2e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -161,6 +161,7 @@ Stefan Zimmermann Stefano Taschini Steffen Allner Stephan Obermann +Tarcisio Fischer Tareq Alayan Ted Xiao Thomas Grainger diff --git a/changelog/2751.bugfix b/changelog/2751.bugfix new file mode 100644 index 000000000..76004a653 --- /dev/null +++ b/changelog/2751.bugfix @@ -0,0 +1 @@ +``testdir`` now uses use the same method used by ``tmpdir`` to create its temporary directory. This changes the final structure of the ``testdir`` directory slightly, but should not affect usage in normal scenarios and avoids a number of potential problems. \ No newline at end of file From d9992558fc0d1eac599ccf54b077ea37e1e06ac0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Sep 2017 19:04:58 -0300 Subject: [PATCH 030/159] Refactor tox.ini so pluggymaster envs share definitions --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 59a151895..fa5565048 100644 --- a/tox.ini +++ b/tox.ini @@ -127,12 +127,12 @@ deps = git+https://github.com/pytest-dev/pluggy.git@master [testenv:py35-pluggymaster] -passenv={[testenv]passenv} -commands={[testenv]commands} +passenv={[testenv:py27-pluggymaster]passenv} +commands={[testenv:py27-pluggymaster]commands} setenv= _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 deps = - {[testenv]deps} + {[testenv:py27-pluggymaster]deps} git+https://github.com/pytest-dev/pluggy.git@master [testenv:docs] From 7d59b2e350cbff1454355bc5225fc0f93bbba3a7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Sep 2017 19:08:20 -0300 Subject: [PATCH 031/159] Fix call to outcome.force_result Even though the test is not running at the moment (xfail), at least we avoid future confusion --- testing/test_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_collection.py b/testing/test_collection.py index 1fc1a5d89..a4ed9f22c 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -281,7 +281,7 @@ class TestPrunetraceback(object): outcome = yield rep = outcome.get_result() rep.headerlines += ["header1"] - outcome.set_result(rep) + outcome.force_result(rep) """) result = testdir.runpytest(p) result.stdout.fnmatch_lines([ From 3d707270213091e231ebadfb526e6da4135685d6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 5 Sep 2017 19:36:53 -0300 Subject: [PATCH 032/159] Improve wording in changelog entry --- changelog/2734.trivial | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2734.trivial b/changelog/2734.trivial index bbf701d16..b3f8471af 100644 --- a/changelog/2734.trivial +++ b/changelog/2734.trivial @@ -1 +1 @@ -- simplify ascii string escaping by using the backslashreplace error handler \ No newline at end of file +Internal refactor: simplify ascii string escaping by using the backslashreplace error handler in newer Python 3 versions. From aa6a67044f1620797ac6dee3faa4e77368970c97 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Wed, 6 Sep 2017 01:13:08 +0200 Subject: [PATCH 033/159] Add match_regex functionality to warns --- _pytest/recwarn.py | 27 ++++++++++++++++++++++----- testing/test_recwarn.py | 18 ++++++++++-------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index c9fa872c0..4f7efba99 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -8,6 +8,8 @@ import py import sys import warnings +import re + from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail @@ -99,9 +101,11 @@ def warns(expected_warning, *args, **kwargs): >>> with warns(RuntimeWarning): ... warnings.warn("my warning", RuntimeWarning) """ - wcheck = WarningsChecker(expected_warning) + match_expr = None if not args: - return wcheck + if "match" in kwargs: + match_expr = kwargs.pop("match") + return WarningsChecker(expected_warning, match_expr=match_expr) elif isinstance(args[0], str): code, = args assert isinstance(code, str) @@ -109,12 +113,12 @@ def warns(expected_warning, *args, **kwargs): loc = frame.f_locals.copy() loc.update(kwargs) - with wcheck: + with WarningsChecker(expected_warning, match_expr=match_expr): code = _pytest._code.Source(code).compile() py.builtin.exec_(code, frame.f_globals, loc) else: func = args[0] - with wcheck: + with WarningsChecker(expected_warning, match_expr=match_expr): return func(*args[1:], **kwargs) @@ -174,7 +178,7 @@ class WarningsRecorder(warnings.catch_warnings): class WarningsChecker(WarningsRecorder): - def __init__(self, expected_warning=None): + def __init__(self, expected_warning=None, match_expr=None): super(WarningsChecker, self).__init__() msg = ("exceptions must be old-style classes or " @@ -189,6 +193,7 @@ class WarningsChecker(WarningsRecorder): 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) @@ -203,3 +208,15 @@ class WarningsChecker(WarningsRecorder): "The list of emitted warnings is: {1}.".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 {0} matching" + " ('{1}') was emitted. The list of emitted warnings" + " is: {2}.".format( + self.expected_warning, + self.match_expr, + [each.message for each in self])) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index e4e7934cc..ca4023f66 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -289,20 +289,22 @@ class TestWarns(object): with pytest.warns(UserWarning, match=r'must be \d+$'): warnings.warn("value must be 42", UserWarning) - with pytest.raises(AssertionError, match='pattern not found'): + with pytest.raises(pytest.fail.Exception): with pytest.warns(UserWarning, match=r'must be \d+$'): warnings.warn("this is not here", UserWarning) - def test_one_from_multiple_warns(): - with warns(UserWarning, match=r'aaa'): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(FutureWarning, match=r'must be \d+$'): + warnings.warn("value must be 42", UserWarning) + + def test_one_from_multiple_warns(self): + with pytest.warns(UserWarning, match=r'aaa'): warnings.warn("cccccccccc", UserWarning) warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("aaaaaaaaaa", UserWarning) - def test_none_of_multiple_warns(): - a, b, c = ('aaa', 'bbbbbbbbbb', 'cccccccccc') - expected_msg = "'{}' pattern not found in \['{}', '{}'\]".format(a, b, c) - with raises(AssertionError, match=expected_msg): - with warns(UserWarning, match=r'aaa'): + def test_none_of_multiple_warns(self): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning, match=r'aaa'): warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("cccccccccc", UserWarning) From 80d165475b4c1ef1f70ed01db3b7b08b627cb91b Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Thu, 7 Sep 2017 10:28:52 +0200 Subject: [PATCH 034/159] Add documentation --- _pytest/recwarn.py | 16 ++++++++++++++++ doc/en/warnings.rst | 15 ++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 4f7efba99..c9f86a483 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -100,6 +100,22 @@ def warns(expected_warning, *args, **kwargs): >>> 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... + """ match_expr = None if not args: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index c84277173..ac26068c4 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -168,7 +168,20 @@ which works in a similar manner to :ref:`raises `:: with pytest.warns(UserWarning): warnings.warn("my warning", UserWarning) -The test will fail if the warning in question is not raised. +The test will fail if the warning in question is not raised. 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... You can also call ``pytest.warns`` on a function or code string:: From a0c6758202c518d7e6c1726a850e6b7e6e0a3a74 Mon Sep 17 00:00:00 2001 From: Joan Massich Date: Thu, 7 Sep 2017 10:51:14 +0200 Subject: [PATCH 035/159] Add changelog --- changelog/2708.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2708.feature diff --git a/changelog/2708.feature b/changelog/2708.feature new file mode 100644 index 000000000..f6039ede9 --- /dev/null +++ b/changelog/2708.feature @@ -0,0 +1 @@ +Match ``warns`` signature to ``raises`` by adding ``match`` keyworkd. \ No newline at end of file From afe7966683903316866bee75fcb3c94414449011 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 18 Sep 2017 21:36:54 -0300 Subject: [PATCH 036/159] Fix call to outcome.get_result now that outcome.result is deprecated --- testing/python/collect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/python/collect.py b/testing/python/collect.py index 6b9c6db4e..ccd5c11e7 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -841,7 +841,7 @@ class TestConftestCustomization(object): def pytest_pycollect_makeitem(): outcome = yield if outcome.excinfo is None: - result = outcome.result + result = outcome.get_result() if result: for func in result: func._some123 = "world" From a2da5a691a63e398223b5588d34e3eeebd0364ad Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 18 Sep 2017 21:38:15 -0300 Subject: [PATCH 037/159] Update tox and appveyor environments to use py36 by default --- .travis.yml | 10 +++++----- appveyor.yml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5694cf355..21fb6c7db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,11 +20,11 @@ env: - TOXENV=py27-trial - TOXENV=py27-numpy - TOXENV=py27-pluggymaster - - TOXENV=py35-pexpect - - TOXENV=py35-xdist - - TOXENV=py35-trial - - TOXENV=py35-numpy - - TOXENV=py35-pluggymaster + - TOXENV=py36-pexpect + - TOXENV=py36-xdist + - TOXENV=py36-trial + - TOXENV=py36-numpy + - TOXENV=py36-pluggymaster - TOXENV=py27-nobyte - TOXENV=doctesting - TOXENV=docs diff --git a/appveyor.yml b/appveyor.yml index ec2611daf..01a723d5f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,11 +22,11 @@ environment: - TOXENV: "py27-trial" - TOXENV: "py27-numpy" - TOXENV: "py27-pluggymaster" - - TOXENV: "py35-pexpect" - - TOXENV: "py35-xdist" - - TOXENV: "py35-trial" - - TOXENV: "py35-numpy" - - TOXENV: "py35-pluggymaster" + - TOXENV: "py36-pexpect" + - TOXENV: "py36-xdist" + - TOXENV: "py36-trial" + - TOXENV: "py36-numpy" + - TOXENV: "py36-pluggymaster" - TOXENV: "py27-nobyte" - TOXENV: "doctesting" - TOXENV: "py35-freeze" From 062a0e3e68fef4f6c927442daf35a3e47e72cd8a Mon Sep 17 00:00:00 2001 From: Ofir Date: Tue, 19 Sep 2017 15:14:08 +0300 Subject: [PATCH 038/159] If an exception happens while loading a plugin, PyTest no longer hides the original traceback. In python2 it will show the original traceback with a new message that explains in which plugin. In python3 it will show 2 canonized exceptions, the original exception while loading the plugin in addition to an exception that PyTest throws about loading a plugin. --- _pytest/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/_pytest/config.py b/_pytest/config.py index 690f98587..595bebe35 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -423,12 +423,12 @@ class PytestPluginManager(PluginManager): try: __import__(importspec) except ImportError as e: - new_exc = ImportError('Error importing plugin "%s": %s' % (modname, safe_str(e.args[0]))) - # copy over name and path attributes - for attr in ('name', 'path'): - if hasattr(e, attr): - setattr(new_exc, attr, getattr(e, attr)) - raise new_exc + new_exc_type = ImportError + new_exc_message = 'Error importing plugin "%s": %s' % (modname, safe_str(e.args[0])) + new_exc = new_exc_type(new_exc_message) + + six.reraise(new_exc_type, new_exc, sys.exc_info()[2]) + except Exception as e: import pytest if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): From c89827b9f2b42352a388fa1a013f6b51d0f33cb8 Mon Sep 17 00:00:00 2001 From: Ofir Date: Tue, 19 Sep 2017 15:23:07 +0300 Subject: [PATCH 039/159] updating import plugin error test in order to make sure it also checks that the original traceback has been shown to the users --- testing/test_pluginmanager.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 2838f83c5..55e2ea10f 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -199,12 +199,17 @@ def test_importplugin_error_message(testdir, pytestpm): testdir.syspathinsert(testdir.tmpdir) testdir.makepyfile(qwe=""" # encoding: UTF-8 - raise ImportError(u'Not possible to import: ☺') + def test_traceback(): + raise ImportError(u'Not possible to import: ☺') + test_traceback() """) with pytest.raises(ImportError) as excinfo: pytestpm.import_plugin("qwe") - expected = '.*Error importing plugin "qwe": Not possible to import: .' - assert py.std.re.match(expected, str(excinfo.value)) + + expected_message = '.*Error importing plugin "qwe": Not possible to import: .' + expected_traceback = ".*in test_traceback" + assert py.std.re.match(expected_message, str(excinfo.value)) + assert py.std.re.match(expected_traceback, str(excinfo.traceback[-1])) class TestPytestPluginManager(object): From b57a84d065846076ab8df6316700ff2ecb0903aa Mon Sep 17 00:00:00 2001 From: Ofir Date: Tue, 19 Sep 2017 15:36:12 +0300 Subject: [PATCH 040/159] updating bugfix changelog --- changelog/2491.bugfix | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/2491.bugfix diff --git a/changelog/2491.bugfix b/changelog/2491.bugfix new file mode 100644 index 000000000..0516b0b5e --- /dev/null +++ b/changelog/2491.bugfix @@ -0,0 +1,4 @@ +If an exception happens while loading a plugin, PyTest no longer hides the original traceback. +In python2 it will show the original traceback with a new message that explains in which plugin. +In python3 it will show 2 canonized exceptions, the original exception while loading the plugin +in addition to an exception that PyTest throws about loading a plugin. \ No newline at end of file From d96869ff66def703c410c4ab258864997adddbd7 Mon Sep 17 00:00:00 2001 From: OfirOshir Date: Wed, 20 Sep 2017 09:45:40 +0300 Subject: [PATCH 041/159] fixing cr --- changelog/2491.bugfix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/2491.bugfix b/changelog/2491.bugfix index 0516b0b5e..fbb16e17e 100644 --- a/changelog/2491.bugfix +++ b/changelog/2491.bugfix @@ -1,4 +1,4 @@ -If an exception happens while loading a plugin, PyTest no longer hides the original traceback. +If an exception happens while loading a plugin, pytest no longer hides the original traceback. In python2 it will show the original traceback with a new message that explains in which plugin. In python3 it will show 2 canonized exceptions, the original exception while loading the plugin -in addition to an exception that PyTest throws about loading a plugin. \ No newline at end of file +in addition to an exception that PyTest throws about loading a plugin. From 8eafbd05ca2d980b36541fbc9d547e52b6016a9a Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 11:25:17 +0200 Subject: [PATCH 042/159] Merge the pytest-catchlog plugin --- _pytest/config.py | 2 +- _pytest/logging.py | 459 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 _pytest/logging.py diff --git a/_pytest/config.py b/_pytest/config.py index 690f98587..0d77cfbbf 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -105,7 +105,7 @@ default_plugins = ( "mark main terminal runner python fixtures debugging unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion " "junitxml resultlog doctest cacheprovider freeze_support " - "setuponly setupplan warnings").split() + "setuponly setupplan warnings logging").split() builtin_plugins = set(default_plugins) diff --git a/_pytest/logging.py b/_pytest/logging.py new file mode 100644 index 000000000..53e704b0d --- /dev/null +++ b/_pytest/logging.py @@ -0,0 +1,459 @@ +from __future__ import absolute_import, division, print_function + +import logging +from contextlib import closing, contextmanager +import functools +import sys + +import pytest +import py + + +DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s' +DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S' + + +def add_option_ini(parser, option, dest, default=None, **kwargs): + parser.addini(dest, default=default, + help='default value for ' + option) + parser.getgroup('catchlog').addoption(option, dest=dest, **kwargs) + + +def get_option_ini(config, name): + ret = config.getoption(name) # 'default' arg won't work as expected + if ret is None: + ret = config.getini(name) + return ret + + +def pytest_addoption(parser): + """Add options to control log capturing.""" + + group = parser.getgroup('catchlog', 'Log catching') + add_option_ini(parser, + '--no-print-logs', + dest='log_print', action='store_const', const=False, default=True, + help='disable printing caught logs on failed tests.' + ) + add_option_ini( + parser, + '--log-level', + dest='log_level', default=None, + help='logging level used by the logging module' + ) + add_option_ini(parser, + '--log-format', + dest='log_format', default=DEFAULT_LOG_FORMAT, + help='log format as used by the logging module.' + ) + add_option_ini(parser, + '--log-date-format', + dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT, + help='log date format as used by the logging module.' + ) + add_option_ini( + parser, + '--log-cli-level', + dest='log_cli_level', default=None, + help='cli logging level.' + ) + add_option_ini( + parser, + '--log-cli-format', + dest='log_cli_format', default=None, + help='log format as used by the logging module.' + ) + add_option_ini( + parser, + '--log-cli-date-format', + dest='log_cli_date_format', default=None, + help='log date format as used by the logging module.' + ) + add_option_ini( + parser, + '--log-file', + dest='log_file', default=None, + help='path to a file when logging will be written to.' + ) + add_option_ini( + parser, + '--log-file-level', + dest='log_file_level', default=None, + help='log file logging level.' + ) + add_option_ini( + parser, + '--log-file-format', + dest='log_file_format', default=DEFAULT_LOG_FORMAT, + help='log format as used by the logging module.' + ) + add_option_ini( + parser, + '--log-file-date-format', + dest='log_file_date_format', default=DEFAULT_LOG_DATE_FORMAT, + help='log date format as used by the logging module.' + ) + + +def get_logger_obj(logger=None): + """Get a logger object that can be specified by its name, or passed as is. + + Defaults to the root logger. + """ + if logger is None or isinstance(logger, py.builtin._basestring): + logger = logging.getLogger(logger) + return logger + + +@contextmanager +def logging_at_level(level, logger=None): + """Context manager that sets the level for capturing of logs.""" + logger = get_logger_obj(logger) + + orig_level = logger.level + logger.setLevel(level) + try: + yield + finally: + logger.setLevel(orig_level) + + +@contextmanager +def logging_using_handler(handler, logger=None): + """Context manager that safely registers a given handler.""" + logger = get_logger_obj(logger) + + if handler in logger.handlers: # reentrancy + # Adding the same handler twice would confuse logging system. + # Just don't do that. + yield + else: + logger.addHandler(handler) + try: + yield + finally: + logger.removeHandler(handler) + + +@contextmanager +def catching_logs(handler, filter=None, formatter=None, + level=logging.NOTSET, logger=None): + """Context manager that prepares the whole logging machinery properly.""" + logger = get_logger_obj(logger) + + if filter is not None: + handler.addFilter(filter) + if formatter is not None: + handler.setFormatter(formatter) + handler.setLevel(level) + + with closing(handler): + with logging_using_handler(handler, logger): + with logging_at_level(min(handler.level, logger.level), logger): + + yield handler + + +class LogCaptureFixture(object): + """Provides access and control of log capturing.""" + + def __init__(self, item): + """Creates a new funcarg.""" + self._item = item + + @property + def handler(self): + return self._item.catch_log_handler + + @property + def text(self): + """Returns the log text.""" + return 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 striped 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] + + def clear(self): + """Reset the list of log records.""" + self.handler.records = [] + + def set_level(self, level, logger=None): + """Sets the level for capturing of logs. + + By default, the level is set on the handler used to capture + logs. Specify a logger name to instead set the level of any + logger. + """ + + obj = logger and logging.getLogger(logger) or self.handler + obj.setLevel(level) + + def at_level(self, level, logger=None): + """Context manager that sets the level for capturing of logs. + + By default, the level is set on the handler used to capture + logs. Specify a logger name to instead set the level of any + logger. + """ + + obj = logger and logging.getLogger(logger) or self.handler + return logging_at_level(level, obj) + + +class CallablePropertyMixin(object): + """Backward compatibility for functions that became properties.""" + + @classmethod + def compat_property(cls, func): + if isinstance(func, property): + make_property = func.getter + func = func.fget + else: + make_property = property + + @functools.wraps(func) + def getter(self): + naked_value = func(self) + ret = cls(naked_value) + ret._naked_value = naked_value + ret._warn_compat = self._warn_compat + ret._prop_name = func.__name__ + return ret + + return make_property(getter) + + def __call__(self): + new = "'caplog.{0}' property".format(self._prop_name) + if self._prop_name == 'records': + new += ' (or caplog.clear())' + self._warn_compat(old="'caplog.{0}()' syntax".format(self._prop_name), + new=new) + return self._naked_value # to let legacy clients modify the object + + +class CallableList(CallablePropertyMixin, list): + pass + + +class CallableStr(CallablePropertyMixin, py.builtin.text): + pass + + +class CompatLogCaptureFixture(LogCaptureFixture): + """Backward compatibility with pytest-capturelog.""" + + def _warn_compat(self, old, new): + self._item.warn(code='L1', + message=("{0} is deprecated, use {1} instead" + .format(old, new))) + + @CallableStr.compat_property + def text(self): + return super(CompatLogCaptureFixture, self).text + + @CallableList.compat_property + def records(self): + return super(CompatLogCaptureFixture, self).records + + @CallableList.compat_property + def record_tuples(self): + return super(CompatLogCaptureFixture, self).record_tuples + + def setLevel(self, level, logger=None): + self._warn_compat(old="'caplog.setLevel()'", + new="'caplog.set_level()'") + return self.set_level(level, logger) + + def atLevel(self, level, logger=None): + self._warn_compat(old="'caplog.atLevel()'", + new="'caplog.at_level()'") + return self.at_level(level, logger) + + +@pytest.fixture +def caplog(request): + """Access and control log capturing. + + Captured logs are available through the following 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 + """ + return CompatLogCaptureFixture(request.node) + + +def get_actual_log_level(config, setting_name): + """Return the actual logging level.""" + log_level = get_option_ini(config, setting_name) + if not log_level: + return + if isinstance(log_level, py.builtin.text): + 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( + "'{0}' is not recognized as a logging level name for " + "'{1}'. Please consider passing the " + "logging level num instead.".format( + log_level, + setting_name)) + + +def pytest_configure(config): + """Always register the log catcher plugin with py.test or tests can't + find the fixture function. + """ + log_cli_level = get_actual_log_level(config, 'log_cli_level') + if log_cli_level is None: + # No specific CLI logging level was provided, let's check + # log_level for a fallback + log_cli_level = get_actual_log_level(config, 'log_level') + if log_cli_level is None: + # No log_level was provided, default to WARNING + log_cli_level = logging.WARNING + config._catchlog_log_cli_level = log_cli_level + config._catchlog_log_file = get_option_ini(config, 'log_file') + if config._catchlog_log_file: + log_file_level = get_actual_log_level(config, 'log_file_level') + if log_file_level is None: + # No log_level was provided, default to WARNING + log_file_level = logging.WARNING + config._catchlog_log_file_level = log_file_level + config.pluginmanager.register(LoggingPlugin(config)) + + +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. + """ + print_logs = get_option_ini(config, 'log_print') + if not isinstance(print_logs, bool): + if print_logs.lower() in ('true', 'yes', '1'): + print_logs = True + elif print_logs.lower() in ('false', 'no', '0'): + print_logs = False + self.print_logs = print_logs + self.formatter = logging.Formatter( + get_option_ini(config, 'log_format'), + get_option_ini(config, 'log_date_format')) + self.log_cli_handler = logging.StreamHandler(sys.stderr) + log_cli_format = get_option_ini(config, 'log_cli_format') + if not log_cli_format: + # No CLI specific format was provided, use log_format + log_cli_format = get_option_ini(config, 'log_format') + log_cli_date_format = get_option_ini(config, 'log_cli_date_format') + if not log_cli_date_format: + # No CLI specific date format was provided, use log_date_format + log_cli_date_format = get_option_ini(config, 'log_date_format') + log_cli_formatter = logging.Formatter( + log_cli_format, + datefmt=log_cli_date_format) + self.log_cli_handler.setFormatter(log_cli_formatter) + if config._catchlog_log_file: + log_file_format = get_option_ini(config, 'log_file_format') + if not log_file_format: + # No log file specific format was provided, use log_format + log_file_format = get_option_ini(config, 'log_format') + log_file_date_format = get_option_ini(config, 'log_file_date_format') + if not log_file_date_format: + # No log file specific date format was provided, use log_date_format + log_file_date_format = get_option_ini(config, 'log_date_format') + self.log_file_handler = logging.FileHandler( + config._catchlog_log_file, + # Each pytest runtests session will write to a clean logfile + mode='w', + ) + log_file_formatter = logging.Formatter( + log_file_format, + datefmt=log_file_date_format) + self.log_file_handler.setFormatter(log_file_formatter) + else: + self.log_file_handler = None + + @contextmanager + def _runtest_for(self, item, when): + """Implements the internals of pytest_runtest_xxx() hook.""" + with catching_logs(LogCaptureHandler(), + formatter=self.formatter) as log_handler: + item.catch_log_handler = log_handler + try: + yield # run test + finally: + del item.catch_log_handler + + 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.mark.hookwrapper + def pytest_runtest_setup(self, item): + with self._runtest_for(item, 'setup'): + yield + + @pytest.mark.hookwrapper + def pytest_runtest_call(self, item): + with self._runtest_for(item, 'call'): + yield + + @pytest.mark.hookwrapper + def pytest_runtest_teardown(self, item): + with self._runtest_for(item, 'teardown'): + yield + + @pytest.mark.hookwrapper + def pytest_runtestloop(self, session): + """Runs all collected test items.""" + with catching_logs(self.log_cli_handler, + level=session.config._catchlog_log_cli_level): + if self.log_file_handler is not None: + with catching_logs(self.log_file_handler, + level=session.config._catchlog_log_file_level): + yield # run all the tests + else: + yield # run all the tests + + +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) + self.stream = py.io.TextIO() + self.records = [] + + def close(self): + """Close this log handler and its underlying stream.""" + + logging.StreamHandler.close(self) + self.stream.close() + + 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) From 6607478b23316653335de85e4a6eb2ec6d7e90c3 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 13:12:07 +0200 Subject: [PATCH 043/159] Add unittests for LoggingPlugin (excluding perf tests) --- _pytest/logging.py | 2 +- testing/logging/conftest.py | 33 +++ testing/logging/test_compat.py | 80 ++++++ testing/logging/test_fixture.py | 99 ++++++++ testing/logging/test_reporting.py | 398 ++++++++++++++++++++++++++++++ 5 files changed, 611 insertions(+), 1 deletion(-) create mode 100644 testing/logging/conftest.py create mode 100644 testing/logging/test_compat.py create mode 100644 testing/logging/test_fixture.py create mode 100644 testing/logging/test_reporting.py diff --git a/_pytest/logging.py b/_pytest/logging.py index 53e704b0d..aa0c46948 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -335,7 +335,7 @@ def pytest_configure(config): # No log_level was provided, default to WARNING log_file_level = logging.WARNING config._catchlog_log_file_level = log_file_level - config.pluginmanager.register(LoggingPlugin(config)) + config.pluginmanager.register(LoggingPlugin(config), 'loggingp') class LoggingPlugin(object): diff --git a/testing/logging/conftest.py b/testing/logging/conftest.py new file mode 100644 index 000000000..9b559d7eb --- /dev/null +++ b/testing/logging/conftest.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +import pytest + + +pytest_plugins = 'pytester' + + +def pytest_addoption(parser): + parser.addoption('--run-perf', + action='store', dest='run_perf', + choices=['yes', 'no', 'only', 'check'], + nargs='?', default='check', const='yes', + help='Run performance tests (can be slow)', + ) + + parser.addoption('--perf-graph', + action='store', dest='perf_graph_name', + nargs='?', default=None, const='graph.svg', + help='Plot a graph using data found in --benchmark-storage', + ) + parser.addoption('--perf-expr', + action='store', dest='perf_expr_primary', + default='log_emit', + help='Benchmark (or expression combining benchmarks) to plot', + ) + parser.addoption('--perf-expr-secondary', + action='store', dest='perf_expr_secondary', + default='caplog - stub', + help=('Benchmark (or expression combining benchmarks) to plot ' + 'as a secondary line'), + ) diff --git a/testing/logging/test_compat.py b/testing/logging/test_compat.py new file mode 100644 index 000000000..0c527b587 --- /dev/null +++ b/testing/logging/test_compat.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +import pytest + + +def test_camel_case_aliases(testdir): + testdir.makepyfile(''' + import logging + + logger = logging.getLogger(__name__) + + def test_foo(caplog): + caplog.setLevel(logging.INFO) + logger.debug('boo!') + + with caplog.atLevel(logging.WARNING): + logger.info('catch me if you can') + ''') + result = testdir.runpytest() + assert result.ret == 0 + + with pytest.raises(pytest.fail.Exception): + result.stdout.fnmatch_lines(['*- Captured *log call -*']) + + result = testdir.runpytest('-rw') + assert result.ret == 0 + result.stdout.fnmatch_lines(''' + =*warning* summary*= + *caplog.setLevel()*deprecated* + *caplog.atLevel()*deprecated* + ''') + + +def test_property_call(testdir): + testdir.makepyfile(''' + import logging + + logger = logging.getLogger(__name__) + + def test_foo(caplog): + logger.info('boo %s', 'arg') + + assert caplog.text == caplog.text() == str(caplog.text) + assert caplog.records == caplog.records() == list(caplog.records) + assert (caplog.record_tuples == + caplog.record_tuples() == list(caplog.record_tuples)) + ''') + result = testdir.runpytest() + assert result.ret == 0 + + result = testdir.runpytest('-rw') + assert result.ret == 0 + result.stdout.fnmatch_lines(''' + =*warning* summary*= + *caplog.text()*deprecated* + *caplog.records()*deprecated* + *caplog.record_tuples()*deprecated* + ''') + + +def test_records_modification(testdir): + testdir.makepyfile(''' + import logging + + logger = logging.getLogger(__name__) + + def test_foo(caplog): + logger.info('boo %s', 'arg') + assert caplog.records + assert caplog.records() + + del caplog.records()[:] # legacy syntax + assert not caplog.records + assert not caplog.records() + + logger.info('foo %s', 'arg') + assert caplog.records + assert caplog.records() + ''') + result = testdir.runpytest() + assert result.ret == 0 diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py new file mode 100644 index 000000000..bdfa67ecc --- /dev/null +++ b/testing/logging/test_fixture.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +import sys +import logging + + +logger = logging.getLogger(__name__) +sublogger = logging.getLogger(__name__+'.baz') + +u = (lambda x: x.decode('utf-8')) if sys.version_info < (3,) else (lambda x: x) + + +def test_fixture_help(testdir): + result = testdir.runpytest('--fixtures') + result.stdout.fnmatch_lines(['*caplog*']) + + +def test_change_level(caplog): + caplog.set_level(logging.INFO) + logger.debug('handler DEBUG level') + logger.info('handler INFO level') + + caplog.set_level(logging.CRITICAL, logger=sublogger.name) + sublogger.warning('logger WARNING level') + sublogger.critical('logger CRITICAL level') + + assert 'DEBUG' not in caplog.text + assert 'INFO' in caplog.text + assert 'WARNING' not in caplog.text + assert 'CRITICAL' in caplog.text + + +def test_with_statement(caplog): + with caplog.at_level(logging.INFO): + logger.debug('handler DEBUG level') + logger.info('handler INFO level') + + with caplog.at_level(logging.CRITICAL, logger=sublogger.name): + sublogger.warning('logger WARNING level') + sublogger.critical('logger CRITICAL level') + + assert 'DEBUG' not in caplog.text + assert 'INFO' in caplog.text + assert 'WARNING' not in caplog.text + assert 'CRITICAL' in caplog.text + + +def test_log_access(caplog): + logger.info('boo %s', 'arg') + assert caplog.records[0].levelname == 'INFO' + assert caplog.records[0].msg == 'boo %s' + assert 'boo arg' in caplog.text + + +def test_record_tuples(caplog): + logger.info('boo %s', 'arg') + + assert caplog.record_tuples == [ + (__name__, logging.INFO, 'boo arg'), + ] + + +def test_unicode(caplog): + logger.info(u('bū')) + assert caplog.records[0].levelname == 'INFO' + assert caplog.records[0].msg == u('bū') + assert u('bū') in caplog.text + + +def test_clear(caplog): + logger.info(u('bū')) + assert len(caplog.records) + caplog.clear() + assert not len(caplog.records) + + +def test_special_warning_with_del_records_warning(testdir): + p1 = testdir.makepyfile(""" + def test_del_records_inline(caplog): + del caplog.records()[:] + """) + result = testdir.runpytest_subprocess(p1) + result.stdout.fnmatch_lines([ + "*'caplog.records()' syntax is deprecated," + " use 'caplog.records' property (or caplog.clear()) instead", + "*1 *warnings*", + ]) + + +def test_warning_with_setLevel(testdir): + p1 = testdir.makepyfile(""" + def test_inline(caplog): + caplog.setLevel(0) + """) + result = testdir.runpytest_subprocess(p1) + result.stdout.fnmatch_lines([ + "*'caplog.setLevel()' is deprecated," + " use 'caplog.set_level()' instead", + "*1 *warnings*", + ]) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py new file mode 100644 index 000000000..af75eb632 --- /dev/null +++ b/testing/logging/test_reporting.py @@ -0,0 +1,398 @@ +# -*- coding: utf-8 -*- +import os +import pytest + + +def test_nothing_logged(testdir): + testdir.makepyfile(''' + import sys + + def test_foo(): + sys.stdout.write('text going to stdout') + sys.stderr.write('text going to stderr') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured stdout call -*', + 'text going to stdout']) + result.stdout.fnmatch_lines(['*- Captured stderr call -*', + 'text going to stderr']) + with pytest.raises(pytest.fail.Exception): + result.stdout.fnmatch_lines(['*- Captured *log call -*']) + + +def test_messages_logged(testdir): + testdir.makepyfile(''' + import sys + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + sys.stdout.write('text going to stdout') + sys.stderr.write('text going to stderr') + logger.info('text going to logger') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured *log call -*', + '*text going to logger*']) + result.stdout.fnmatch_lines(['*- Captured stdout call -*', + 'text going to stdout']) + result.stdout.fnmatch_lines(['*- Captured stderr call -*', + 'text going to stderr']) + + +def test_setup_logging(testdir): + testdir.makepyfile(''' + import logging + + logger = logging.getLogger(__name__) + + def setup_function(function): + logger.info('text going to logger from setup') + + def test_foo(): + logger.info('text going to logger from call') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured *log setup -*', + '*text going to logger from setup*', + '*- Captured *log call -*', + '*text going to logger from call*']) + + +def test_teardown_logging(testdir): + testdir.makepyfile(''' + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.info('text going to logger from call') + + def teardown_function(function): + logger.info('text going to logger from teardown') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured *log call -*', + '*text going to logger from call*', + '*- Captured *log teardown -*', + '*text going to logger from teardown*']) + + +def test_disable_log_capturing(testdir): + testdir.makepyfile(''' + import sys + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + sys.stdout.write('text going to stdout') + logger.warning('catch me if you can!') + sys.stderr.write('text going to stderr') + assert False + ''') + result = testdir.runpytest('--no-print-logs') + print(result.stdout) + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured stdout call -*', + 'text going to stdout']) + result.stdout.fnmatch_lines(['*- Captured stderr call -*', + 'text going to stderr']) + with pytest.raises(pytest.fail.Exception): + result.stdout.fnmatch_lines(['*- Captured *log call -*']) + + +def test_disable_log_capturing_ini(testdir): + testdir.makeini( + ''' + [pytest] + log_print=False + ''' + ) + testdir.makepyfile(''' + import sys + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + sys.stdout.write('text going to stdout') + logger.warning('catch me if you can!') + sys.stderr.write('text going to stderr') + assert False + ''') + result = testdir.runpytest() + print(result.stdout) + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured stdout call -*', + 'text going to stdout']) + result.stdout.fnmatch_lines(['*- Captured stderr call -*', + 'text going to stderr']) + with pytest.raises(pytest.fail.Exception): + result.stdout.fnmatch_lines(['*- Captured *log call -*']) + + +def test_log_cli_default_level(testdir): + # Default log file level + testdir.makepyfile(''' + import pytest + import logging + def test_log_cli(request): + plugin = request.config.pluginmanager.getplugin('loggingp') + assert plugin.log_cli_handler.level == logging.WARNING + logging.getLogger('catchlog').info("This log message won't be shown") + logging.getLogger('catchlog').warning("This log message will be shown") + print('PASSED') + ''') + + result = testdir.runpytest('-s') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_cli_default_level.py PASSED', + ]) + result.stderr.fnmatch_lines([ + "* This log message will be shown" + ]) + for line in result.errlines: + try: + assert "This log message won't be shown" in line + pytest.fail("A log message was shown and it shouldn't have been") + except AssertionError: + continue + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + + +def test_log_cli_level(testdir): + # Default log file level + testdir.makepyfile(''' + import pytest + import logging + def test_log_cli(request): + plugin = request.config.pluginmanager.getplugin('loggingp') + assert plugin.log_cli_handler.level == logging.INFO + logging.getLogger('catchlog').debug("This log message won't be shown") + logging.getLogger('catchlog').info("This log message will be shown") + print('PASSED') + ''') + + result = testdir.runpytest('-s', '--log-cli-level=INFO') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_cli_level.py PASSED', + ]) + result.stderr.fnmatch_lines([ + "* This log message will be shown" + ]) + for line in result.errlines: + try: + assert "This log message won't be shown" in line + pytest.fail("A log message was shown and it shouldn't have been") + except AssertionError: + continue + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + + result = testdir.runpytest('-s', '--log-level=INFO') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_cli_level.py PASSED', + ]) + result.stderr.fnmatch_lines([ + "* This log message will be shown" + ]) + for line in result.errlines: + try: + assert "This log message won't be shown" in line + pytest.fail("A log message was shown and it shouldn't have been") + except AssertionError: + continue + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + + +def test_log_cli_ini_level(testdir): + testdir.makeini( + """ + [pytest] + log_cli_level = INFO + """) + testdir.makepyfile(''' + import pytest + import logging + def test_log_cli(request): + plugin = request.config.pluginmanager.getplugin('loggingp') + assert plugin.log_cli_handler.level == logging.INFO + logging.getLogger('catchlog').debug("This log message won't be shown") + logging.getLogger('catchlog').info("This log message will be shown") + print('PASSED') + ''') + + result = testdir.runpytest('-s') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_cli_ini_level.py PASSED', + ]) + result.stderr.fnmatch_lines([ + "* This log message will be shown" + ]) + for line in result.errlines: + try: + assert "This log message won't be shown" in line + pytest.fail("A log message was shown and it shouldn't have been") + except AssertionError: + continue + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + + +def test_log_file_cli(testdir): + # Default log file level + testdir.makepyfile(''' + import pytest + import logging + def test_log_file(request): + plugin = request.config.pluginmanager.getplugin('loggingp') + assert plugin.log_file_handler.level == logging.WARNING + logging.getLogger('catchlog').info("This log message won't be shown") + logging.getLogger('catchlog').warning("This log message will be shown") + print('PASSED') + ''') + + log_file = testdir.tmpdir.join('pytest.log').strpath + + result = testdir.runpytest('-s', '--log-file={0}'.format(log_file)) + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_file_cli.py PASSED', + ]) + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + assert os.path.isfile(log_file) + with open(log_file) as rfh: + contents = rfh.read() + assert "This log message will be shown" in contents + assert "This log message won't be shown" not in contents + + +def test_log_file_cli_level(testdir): + # Default log file level + testdir.makepyfile(''' + import pytest + import logging + def test_log_file(request): + plugin = request.config.pluginmanager.getplugin('loggingp') + assert plugin.log_file_handler.level == logging.INFO + logging.getLogger('catchlog').debug("This log message won't be shown") + logging.getLogger('catchlog').info("This log message will be shown") + print('PASSED') + ''') + + log_file = testdir.tmpdir.join('pytest.log').strpath + + result = testdir.runpytest('-s', + '--log-file={0}'.format(log_file), + '--log-file-level=INFO') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_file_cli_level.py PASSED', + ]) + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + assert os.path.isfile(log_file) + with open(log_file) as rfh: + contents = rfh.read() + assert "This log message will be shown" in contents + assert "This log message won't be shown" not in contents + + +def test_log_file_ini(testdir): + log_file = testdir.tmpdir.join('pytest.log').strpath + + testdir.makeini( + """ + [pytest] + log_file={0} + """.format(log_file)) + testdir.makepyfile(''' + import pytest + import logging + def test_log_file(request): + plugin = request.config.pluginmanager.getplugin('loggingp') + assert plugin.log_file_handler.level == logging.WARNING + logging.getLogger('catchlog').info("This log message won't be shown") + logging.getLogger('catchlog').warning("This log message will be shown") + print('PASSED') + ''') + + result = testdir.runpytest('-s') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_file_ini.py PASSED', + ]) + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + assert os.path.isfile(log_file) + with open(log_file) as rfh: + contents = rfh.read() + assert "This log message will be shown" in contents + assert "This log message won't be shown" not in contents + + +def test_log_file_ini_level(testdir): + log_file = testdir.tmpdir.join('pytest.log').strpath + + testdir.makeini( + """ + [pytest] + log_file={0} + log_file_level = INFO + """.format(log_file)) + testdir.makepyfile(''' + import pytest + import logging + def test_log_file(request): + plugin = request.config.pluginmanager.getplugin('loggingp') + assert plugin.log_file_handler.level == logging.INFO + logging.getLogger('catchlog').debug("This log message won't be shown") + logging.getLogger('catchlog').info("This log message will be shown") + print('PASSED') + ''') + + result = testdir.runpytest('-s') + + # fnmatch_lines does an assertion internally + result.stdout.fnmatch_lines([ + 'test_log_file_ini_level.py PASSED', + ]) + + # make sure that that we get a '0' exit code for the testsuite + assert result.ret == 0 + assert os.path.isfile(log_file) + with open(log_file) as rfh: + contents = rfh.read() + assert "This log message will be shown" in contents + assert "This log message won't be shown" not in contents From 5130f5707f2652262ca3fb2d2c9611824611c5eb Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 13:23:29 +0200 Subject: [PATCH 044/159] Fix name clash --- testing/logging/{test_compat.py => test_capturelog_compat.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename testing/logging/{test_compat.py => test_capturelog_compat.py} (100%) diff --git a/testing/logging/test_compat.py b/testing/logging/test_capturelog_compat.py similarity index 100% rename from testing/logging/test_compat.py rename to testing/logging/test_capturelog_compat.py From ca46f4fe2a870d858ec5678116ea5d3b865489fe Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 13:47:48 +0200 Subject: [PATCH 045/159] Remove conftest --- testing/logging/conftest.py | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 testing/logging/conftest.py diff --git a/testing/logging/conftest.py b/testing/logging/conftest.py deleted file mode 100644 index 9b559d7eb..000000000 --- a/testing/logging/conftest.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function - -import pytest - - -pytest_plugins = 'pytester' - - -def pytest_addoption(parser): - parser.addoption('--run-perf', - action='store', dest='run_perf', - choices=['yes', 'no', 'only', 'check'], - nargs='?', default='check', const='yes', - help='Run performance tests (can be slow)', - ) - - parser.addoption('--perf-graph', - action='store', dest='perf_graph_name', - nargs='?', default=None, const='graph.svg', - help='Plot a graph using data found in --benchmark-storage', - ) - parser.addoption('--perf-expr', - action='store', dest='perf_expr_primary', - default='log_emit', - help='Benchmark (or expression combining benchmarks) to plot', - ) - parser.addoption('--perf-expr-secondary', - action='store', dest='perf_expr_secondary', - default='caplog - stub', - help=('Benchmark (or expression combining benchmarks) to plot ' - 'as a secondary line'), - ) From a8e3effb6c25129bc828df02fa9234b1f7aea1e4 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 15:04:26 +0200 Subject: [PATCH 046/159] Upgrade py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4d74e6bca..27d066fba 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def has_environment_marker_support(): def main(): - install_requires = ['py>=1.4.33', 'six>=1.10.0', 'setuptools'] + install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools'] # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ: From 1ba219e0da9a62a82dc6ad3c154c4df948a5bf32 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 15:08:31 +0200 Subject: [PATCH 047/159] Adapt (logging) unittest --- testing/test_capture.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index eb10f3c07..841cdb0a3 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -346,8 +346,6 @@ class TestLoggingInteraction(object): p = testdir.makepyfile(""" import sys def test_something(): - # pytest does not import logging - assert 'logging' not in sys.modules import logging logging.basicConfig() logging.warn("hello432") From 1bea7e698553662ce52111f3b2f68f3c625b2df2 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 15:32:23 +0200 Subject: [PATCH 048/159] Cleanup pytest_addoption --- _pytest/logging.py | 61 ++++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index aa0c46948..5d5a8522a 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -13,12 +13,6 @@ DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s' DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S' -def add_option_ini(parser, option, dest, default=None, **kwargs): - parser.addini(dest, default=default, - help='default value for ' + option) - parser.getgroup('catchlog').addoption(option, dest=dest, **kwargs) - - def get_option_ini(config, name): ret = config.getoption(name) # 'default' arg won't work as expected if ret is None: @@ -29,70 +23,57 @@ def get_option_ini(config, name): def pytest_addoption(parser): """Add options to control log capturing.""" - group = parser.getgroup('catchlog', 'Log catching') - add_option_ini(parser, + group = parser.getgroup('logging') + + def add_option_ini(option, dest, default=None, **kwargs): + parser.addini(dest, default=default, + 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, - help='disable printing caught logs on failed tests.' - ) + help='disable printing caught logs on failed tests.') add_option_ini( - parser, '--log-level', dest='log_level', default=None, - help='logging level used by the logging module' - ) - add_option_ini(parser, + 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(parser, + 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.' - ) + help='log date format as used by the logging module.') add_option_ini( - parser, '--log-cli-level', dest='log_cli_level', default=None, - help='cli logging level.' - ) + help='cli logging level.') add_option_ini( - parser, '--log-cli-format', dest='log_cli_format', default=None, - help='log format as used by the logging module.' - ) + help='log format as used by the logging module.') add_option_ini( - parser, '--log-cli-date-format', dest='log_cli_date_format', default=None, - help='log date format as used by the logging module.' - ) + help='log date format as used by the logging module.') add_option_ini( - parser, '--log-file', dest='log_file', default=None, - help='path to a file when logging will be written to.' - ) + help='path to a file when logging will be written to.') add_option_ini( - parser, '--log-file-level', dest='log_file_level', default=None, - help='log file logging level.' - ) + help='log file logging level.') add_option_ini( - parser, '--log-file-format', dest='log_file_format', default=DEFAULT_LOG_FORMAT, - help='log format as used by the logging module.' - ) + help='log format as used by the logging module.') add_option_ini( - parser, '--log-file-date-format', dest='log_file_date_format', default=DEFAULT_LOG_DATE_FORMAT, - help='log date format as used by the logging module.' - ) + help='log date format as used by the logging module.') def get_logger_obj(logger=None): From 98209e92ee3ac1dec888eb123141488f265d9082 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 18:01:58 +0200 Subject: [PATCH 049/159] Remove superfluous whitespace in docstring --- _pytest/logging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 5d5a8522a..36ce3e3b8 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -297,8 +297,8 @@ def get_actual_log_level(config, setting_name): def pytest_configure(config): - """Always register the log catcher plugin with py.test or tests can't - find the fixture function. + """Always register the logging plugin with py.test or tests can't + find the fixture function. """ log_cli_level = get_actual_log_level(config, 'log_cli_level') if log_cli_level is None: From 3e71a50403b6eeaee9d309f9a67bac98b67a5629 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 18:02:28 +0200 Subject: [PATCH 050/159] Remove unneeded sys import from unittest --- testing/test_capture.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 841cdb0a3..79e34a40f 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -344,7 +344,6 @@ class TestLoggingInteraction(object): def test_logging_initialized_in_test(self, testdir): p = testdir.makepyfile(""" - import sys def test_something(): import logging logging.basicConfig() From 36cceeb10eaed8310b4cc1b6761879d47ce07d72 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 18:09:27 +0200 Subject: [PATCH 051/159] Set type of log_print ini-variable to 'bool' --- _pytest/logging.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 36ce3e3b8..6e4a9e724 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -25,14 +25,15 @@ def pytest_addoption(parser): group = parser.getgroup('logging') - def add_option_ini(option, dest, default=None, **kwargs): - parser.addini(dest, default=default, + 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', @@ -329,13 +330,7 @@ class LoggingPlugin(object): The formatter can be safely shared across all handlers so create a single one for the entire test session here. """ - print_logs = get_option_ini(config, 'log_print') - if not isinstance(print_logs, bool): - if print_logs.lower() in ('true', 'yes', '1'): - print_logs = True - elif print_logs.lower() in ('false', 'no', '0'): - print_logs = False - self.print_logs = print_logs + self.print_logs = get_option_ini(config, 'log_print') self.formatter = logging.Formatter( get_option_ini(config, 'log_format'), get_option_ini(config, 'log_date_format')) From a1bd54e4eae50d39a7650e93cd39fc09ce8c3cac Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 13 Sep 2017 23:56:35 +0200 Subject: [PATCH 052/159] Clean-up LogCaptureHandler --- _pytest/logging.py | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 6e4a9e724..1043cffda 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -136,6 +136,22 @@ def catching_logs(handler, filter=None, formatter=None, yield 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) + + class LogCaptureFixture(object): """Provides access and control of log capturing.""" @@ -410,26 +426,3 @@ class LoggingPlugin(object): yield # run all the tests else: yield # run all the tests - - -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) - self.stream = py.io.TextIO() - self.records = [] - - def close(self): - """Close this log handler and its underlying stream.""" - - logging.StreamHandler.close(self) - self.stream.close() - - 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) From fc965c1dc569556323fc196bd026f877e42a5d6a Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Thu, 14 Sep 2017 00:17:51 +0200 Subject: [PATCH 053/159] Remove outdated docstring --- _pytest/logging.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 1043cffda..c4a63db2d 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -314,9 +314,6 @@ def get_actual_log_level(config, setting_name): def pytest_configure(config): - """Always register the logging plugin with py.test or tests can't - find the fixture function. - """ log_cli_level = get_actual_log_level(config, 'log_cli_level') if log_cli_level is None: # No specific CLI logging level was provided, let's check From 87b8dc5afbb1a80670b510fbf40b9018f04a43d1 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 15 Sep 2017 03:04:23 +0200 Subject: [PATCH 054/159] Move 'config' handling from pytest_configure to __init__ --- _pytest/logging.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index c4a63db2d..fd5effb20 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -314,22 +314,6 @@ def get_actual_log_level(config, setting_name): def pytest_configure(config): - log_cli_level = get_actual_log_level(config, 'log_cli_level') - if log_cli_level is None: - # No specific CLI logging level was provided, let's check - # log_level for a fallback - log_cli_level = get_actual_log_level(config, 'log_level') - if log_cli_level is None: - # No log_level was provided, default to WARNING - log_cli_level = logging.WARNING - config._catchlog_log_cli_level = log_cli_level - config._catchlog_log_file = get_option_ini(config, 'log_file') - if config._catchlog_log_file: - log_file_level = get_actual_log_level(config, 'log_file_level') - if log_file_level is None: - # No log_level was provided, default to WARNING - log_file_level = logging.WARNING - config._catchlog_log_file_level = log_file_level config.pluginmanager.register(LoggingPlugin(config), 'loggingp') @@ -343,6 +327,16 @@ class LoggingPlugin(object): The formatter can be safely shared across all handlers so create a single one for the entire test session here. """ + log_cli_level = get_actual_log_level(config, 'log_cli_level') + if log_cli_level is None: + # No specific CLI logging level was provided, let's check + # log_level for a fallback + log_cli_level = get_actual_log_level(config, 'log_level') + if log_cli_level is None: + # No log_level was provided, default to WARNING + log_cli_level = logging.WARNING + config._catchlog_log_cli_level = log_cli_level + self.print_logs = get_option_ini(config, 'log_print') self.formatter = logging.Formatter( get_option_ini(config, 'log_format'), @@ -360,7 +354,15 @@ class LoggingPlugin(object): log_cli_format, datefmt=log_cli_date_format) self.log_cli_handler.setFormatter(log_cli_formatter) - if config._catchlog_log_file: + + log_file = get_option_ini(config, 'log_file') + if log_file: + log_file_level = get_actual_log_level(config, 'log_file_level') + if log_file_level is None: + # No log_level was provided, default to WARNING + log_file_level = logging.WARNING + config._catchlog_log_file_level = log_file_level + log_file_format = get_option_ini(config, 'log_file_format') if not log_file_format: # No log file specific format was provided, use log_format @@ -370,7 +372,7 @@ class LoggingPlugin(object): # No log file specific date format was provided, use log_date_format log_file_date_format = get_option_ini(config, 'log_date_format') self.log_file_handler = logging.FileHandler( - config._catchlog_log_file, + log_file, # Each pytest runtests session will write to a clean logfile mode='w', ) From f1f6109255e5d2ef021f07d2b42d5f8c5f189f52 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 15 Sep 2017 03:11:56 +0200 Subject: [PATCH 055/159] Remove _catchlog_ prefix --- _pytest/logging.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index fd5effb20..8fce433f5 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -335,7 +335,7 @@ class LoggingPlugin(object): if log_cli_level is None: # No log_level was provided, default to WARNING log_cli_level = logging.WARNING - config._catchlog_log_cli_level = log_cli_level + config.log_cli_level = log_cli_level self.print_logs = get_option_ini(config, 'log_print') self.formatter = logging.Formatter( @@ -361,7 +361,7 @@ class LoggingPlugin(object): if log_file_level is None: # No log_level was provided, default to WARNING log_file_level = logging.WARNING - config._catchlog_log_file_level = log_file_level + config.log_file_level = log_file_level log_file_format = get_option_ini(config, 'log_file_format') if not log_file_format: @@ -418,10 +418,10 @@ class LoggingPlugin(object): def pytest_runtestloop(self, session): """Runs all collected test items.""" with catching_logs(self.log_cli_handler, - level=session.config._catchlog_log_cli_level): + level=session.config.log_cli_level): if self.log_file_handler is not None: with catching_logs(self.log_file_handler, - level=session.config._catchlog_log_file_level): + level=session.config.log_file_level): yield # run all the tests else: yield # run all the tests From d13e17cf51abf424f0bd26a3f8607c8f8339b390 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 15 Sep 2017 03:15:07 +0200 Subject: [PATCH 056/159] Don't modify the 'config' object in __init__ --- _pytest/logging.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 8fce433f5..aa8cc7b8d 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -335,7 +335,7 @@ class LoggingPlugin(object): if log_cli_level is None: # No log_level was provided, default to WARNING log_cli_level = logging.WARNING - config.log_cli_level = log_cli_level + self.log_cli_level = log_cli_level self.print_logs = get_option_ini(config, 'log_print') self.formatter = logging.Formatter( @@ -361,7 +361,7 @@ class LoggingPlugin(object): if log_file_level is None: # No log_level was provided, default to WARNING log_file_level = logging.WARNING - config.log_file_level = log_file_level + self.log_file_level = log_file_level log_file_format = get_option_ini(config, 'log_file_format') if not log_file_format: @@ -418,10 +418,10 @@ class LoggingPlugin(object): def pytest_runtestloop(self, session): """Runs all collected test items.""" with catching_logs(self.log_cli_handler, - level=session.config.log_cli_level): + level=self.log_cli_level): if self.log_file_handler is not None: with catching_logs(self.log_file_handler, - level=session.config.log_file_level): + level=self.log_file_level): yield # run all the tests else: yield # run all the tests From 08f6b5f4ea72add0fd07e4f6f559765a7bd8accf Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 15 Sep 2017 04:43:51 +0200 Subject: [PATCH 057/159] Use pytest.hookimpl instead of pytest.mark.hookwrapper pytest.mark.hookwrapper seems to be used nowhere in the _pytest package. --- _pytest/logging.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index aa8cc7b8d..2f541245c 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -399,22 +399,22 @@ class LoggingPlugin(object): log = log_handler.stream.getvalue().strip() item.add_report_section(when, 'log', log) - @pytest.mark.hookwrapper + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): with self._runtest_for(item, 'setup'): yield - @pytest.mark.hookwrapper + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): with self._runtest_for(item, 'call'): yield - @pytest.mark.hookwrapper + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): with self._runtest_for(item, 'teardown'): yield - @pytest.mark.hookwrapper + @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session): """Runs all collected test items.""" with catching_logs(self.log_cli_handler, From e41fd52e8c6b6bc853511244c9a5459b218041c0 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 15 Sep 2017 12:45:25 +0200 Subject: [PATCH 058/159] Introduce live_logs context manager --- _pytest/logging.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 2f541245c..909a566dc 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -341,7 +341,8 @@ class LoggingPlugin(object): self.formatter = logging.Formatter( get_option_ini(config, 'log_format'), get_option_ini(config, 'log_date_format')) - self.log_cli_handler = logging.StreamHandler(sys.stderr) + + log_cli_handler = logging.StreamHandler(sys.stderr) log_cli_format = get_option_ini(config, 'log_cli_format') if not log_cli_format: # No CLI specific format was provided, use log_format @@ -353,7 +354,10 @@ class LoggingPlugin(object): log_cli_formatter = logging.Formatter( log_cli_format, datefmt=log_cli_date_format) - self.log_cli_handler.setFormatter(log_cli_formatter) + log_cli_handler.setFormatter(log_cli_formatter) + self.log_cli_handler = log_cli_handler # needed for a single unittest + self.live_logs = catching_logs(log_cli_handler, + level=self.log_cli_level) log_file = get_option_ini(config, 'log_file') if log_file: @@ -417,8 +421,7 @@ class LoggingPlugin(object): @pytest.hookimpl(hookwrapper=True) def pytest_runtestloop(self, session): """Runs all collected test items.""" - with catching_logs(self.log_cli_handler, - level=self.log_cli_level): + with self.live_logs: if self.log_file_handler is not None: with catching_logs(self.log_file_handler, level=self.log_file_level): From 57f66a455aa51fb0a38578664156923c5a8a5adf Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 15 Sep 2017 13:33:54 +0200 Subject: [PATCH 059/159] catching_logs: Remove unused 'filter' kwarg --- _pytest/logging.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 909a566dc..77ae04424 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -118,13 +118,11 @@ def logging_using_handler(handler, logger=None): @contextmanager -def catching_logs(handler, filter=None, formatter=None, +def catching_logs(handler, formatter=None, level=logging.NOTSET, logger=None): """Context manager that prepares the whole logging machinery properly.""" logger = get_logger_obj(logger) - if filter is not None: - handler.addFilter(filter) if formatter is not None: handler.setFormatter(formatter) handler.setLevel(level) From 3a4011585f67981595d6a2eb8ca3de78bbe2f251 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 15 Sep 2017 13:38:49 +0200 Subject: [PATCH 060/159] catching_logs: Remove usage of 'closing' ctx manager The 'closing' context manager is only needed for the log_file_handler. --- _pytest/logging.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 77ae04424..e9b143369 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -127,11 +127,9 @@ def catching_logs(handler, formatter=None, handler.setFormatter(formatter) handler.setLevel(level) - with closing(handler): - with logging_using_handler(handler, logger): - with logging_at_level(min(handler.level, logger.level), logger): - - yield handler + with logging_using_handler(handler, logger): + with logging_at_level(min(handler.level, logger.level), logger): + yield handler class LogCaptureHandler(logging.StreamHandler): @@ -421,8 +419,9 @@ class LoggingPlugin(object): """Runs all collected test items.""" with self.live_logs: 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 + with closing(self.log_file_handler): + with catching_logs(self.log_file_handler, + level=self.log_file_level): + yield # run all the tests else: yield # run all the tests From 207f153ec13e41e13697da672fe59a64e320f00a Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Sun, 17 Sep 2017 14:11:35 +0200 Subject: [PATCH 061/159] Remove logging_at_level ctx manager --- _pytest/logging.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index e9b143369..cec37b641 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -87,19 +87,6 @@ def get_logger_obj(logger=None): return logger -@contextmanager -def logging_at_level(level, logger=None): - """Context manager that sets the level for capturing of logs.""" - logger = get_logger_obj(logger) - - orig_level = logger.level - logger.setLevel(level) - try: - yield - finally: - logger.setLevel(orig_level) - - @contextmanager def logging_using_handler(handler, logger=None): """Context manager that safely registers a given handler.""" @@ -128,8 +115,12 @@ def catching_logs(handler, formatter=None, handler.setLevel(level) with logging_using_handler(handler, logger): - with logging_at_level(min(handler.level, logger.level), logger): + orig_level = logger.level + logger.setLevel(min(orig_level, level)) + try: yield handler + finally: + logger.setLevel(orig_level) class LogCaptureHandler(logging.StreamHandler): @@ -195,6 +186,7 @@ class LogCaptureFixture(object): obj = logger and logging.getLogger(logger) or self.handler obj.setLevel(level) + @contextmanager def at_level(self, level, logger=None): """Context manager that sets the level for capturing of logs. @@ -202,9 +194,17 @@ class LogCaptureFixture(object): logs. Specify a logger name to instead set the level of any logger. """ + if logger is None: + logger = self.handler + else: + logger = logging.getLogger(logger) - obj = logger and logging.getLogger(logger) or self.handler - return logging_at_level(level, obj) + orig_level = logger.level + logger.setLevel(level) + try: + yield + finally: + logger.setLevel(orig_level) class CallablePropertyMixin(object): From 2559ec8bdbaa71f5bc127e2f3fe50b79b98a2ad4 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Sun, 17 Sep 2017 14:14:13 +0200 Subject: [PATCH 062/159] use 'formatter' kwarg of catching_logs --- _pytest/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index cec37b641..57ed4035b 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -350,9 +350,9 @@ class LoggingPlugin(object): log_cli_formatter = logging.Formatter( log_cli_format, datefmt=log_cli_date_format) - log_cli_handler.setFormatter(log_cli_formatter) self.log_cli_handler = log_cli_handler # needed for a single unittest self.live_logs = catching_logs(log_cli_handler, + formatter=log_cli_formatter, level=self.log_cli_level) log_file = get_option_ini(config, 'log_file') From ad21d5cac4a65d3b2d167453d1f92f053b86f5fb Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Sun, 17 Sep 2017 14:28:18 +0200 Subject: [PATCH 063/159] Remove pytest-capturelog backward compat code --- _pytest/logging.py | 81 ++--------------------- testing/logging/test_capturelog_compat.py | 80 ---------------------- testing/logging/test_fixture.py | 26 -------- 3 files changed, 6 insertions(+), 181 deletions(-) delete mode 100644 testing/logging/test_capturelog_compat.py diff --git a/_pytest/logging.py b/_pytest/logging.py index 57ed4035b..b8533da2f 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, division, print_function import logging from contextlib import closing, contextmanager -import functools import sys import pytest @@ -182,9 +181,11 @@ class LogCaptureFixture(object): logs. Specify a logger name to instead set the level of any logger. """ - - obj = logger and logging.getLogger(logger) or self.handler - obj.setLevel(level) + if logger is None: + logger = self.handler + else: + logger = logging.getLogger(logger) + logger.setLevel(level) @contextmanager def at_level(self, level, logger=None): @@ -207,76 +208,6 @@ class LogCaptureFixture(object): logger.setLevel(orig_level) -class CallablePropertyMixin(object): - """Backward compatibility for functions that became properties.""" - - @classmethod - def compat_property(cls, func): - if isinstance(func, property): - make_property = func.getter - func = func.fget - else: - make_property = property - - @functools.wraps(func) - def getter(self): - naked_value = func(self) - ret = cls(naked_value) - ret._naked_value = naked_value - ret._warn_compat = self._warn_compat - ret._prop_name = func.__name__ - return ret - - return make_property(getter) - - def __call__(self): - new = "'caplog.{0}' property".format(self._prop_name) - if self._prop_name == 'records': - new += ' (or caplog.clear())' - self._warn_compat(old="'caplog.{0}()' syntax".format(self._prop_name), - new=new) - return self._naked_value # to let legacy clients modify the object - - -class CallableList(CallablePropertyMixin, list): - pass - - -class CallableStr(CallablePropertyMixin, py.builtin.text): - pass - - -class CompatLogCaptureFixture(LogCaptureFixture): - """Backward compatibility with pytest-capturelog.""" - - def _warn_compat(self, old, new): - self._item.warn(code='L1', - message=("{0} is deprecated, use {1} instead" - .format(old, new))) - - @CallableStr.compat_property - def text(self): - return super(CompatLogCaptureFixture, self).text - - @CallableList.compat_property - def records(self): - return super(CompatLogCaptureFixture, self).records - - @CallableList.compat_property - def record_tuples(self): - return super(CompatLogCaptureFixture, self).record_tuples - - def setLevel(self, level, logger=None): - self._warn_compat(old="'caplog.setLevel()'", - new="'caplog.set_level()'") - return self.set_level(level, logger) - - def atLevel(self, level, logger=None): - self._warn_compat(old="'caplog.atLevel()'", - new="'caplog.at_level()'") - return self.at_level(level, logger) - - @pytest.fixture def caplog(request): """Access and control log capturing. @@ -287,7 +218,7 @@ def caplog(request): * caplog.records() -> list of logging.LogRecord instances * caplog.record_tuples() -> list of (logger_name, level, message) tuples """ - return CompatLogCaptureFixture(request.node) + return LogCaptureFixture(request.node) def get_actual_log_level(config, setting_name): diff --git a/testing/logging/test_capturelog_compat.py b/testing/logging/test_capturelog_compat.py deleted file mode 100644 index 0c527b587..000000000 --- a/testing/logging/test_capturelog_compat.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - - -def test_camel_case_aliases(testdir): - testdir.makepyfile(''' - import logging - - logger = logging.getLogger(__name__) - - def test_foo(caplog): - caplog.setLevel(logging.INFO) - logger.debug('boo!') - - with caplog.atLevel(logging.WARNING): - logger.info('catch me if you can') - ''') - result = testdir.runpytest() - assert result.ret == 0 - - with pytest.raises(pytest.fail.Exception): - result.stdout.fnmatch_lines(['*- Captured *log call -*']) - - result = testdir.runpytest('-rw') - assert result.ret == 0 - result.stdout.fnmatch_lines(''' - =*warning* summary*= - *caplog.setLevel()*deprecated* - *caplog.atLevel()*deprecated* - ''') - - -def test_property_call(testdir): - testdir.makepyfile(''' - import logging - - logger = logging.getLogger(__name__) - - def test_foo(caplog): - logger.info('boo %s', 'arg') - - assert caplog.text == caplog.text() == str(caplog.text) - assert caplog.records == caplog.records() == list(caplog.records) - assert (caplog.record_tuples == - caplog.record_tuples() == list(caplog.record_tuples)) - ''') - result = testdir.runpytest() - assert result.ret == 0 - - result = testdir.runpytest('-rw') - assert result.ret == 0 - result.stdout.fnmatch_lines(''' - =*warning* summary*= - *caplog.text()*deprecated* - *caplog.records()*deprecated* - *caplog.record_tuples()*deprecated* - ''') - - -def test_records_modification(testdir): - testdir.makepyfile(''' - import logging - - logger = logging.getLogger(__name__) - - def test_foo(caplog): - logger.info('boo %s', 'arg') - assert caplog.records - assert caplog.records() - - del caplog.records()[:] # legacy syntax - assert not caplog.records - assert not caplog.records() - - logger.info('foo %s', 'arg') - assert caplog.records - assert caplog.records() - ''') - result = testdir.runpytest() - assert result.ret == 0 diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index bdfa67ecc..4072234d8 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -71,29 +71,3 @@ def test_clear(caplog): assert len(caplog.records) caplog.clear() assert not len(caplog.records) - - -def test_special_warning_with_del_records_warning(testdir): - p1 = testdir.makepyfile(""" - def test_del_records_inline(caplog): - del caplog.records()[:] - """) - result = testdir.runpytest_subprocess(p1) - result.stdout.fnmatch_lines([ - "*'caplog.records()' syntax is deprecated," - " use 'caplog.records' property (or caplog.clear()) instead", - "*1 *warnings*", - ]) - - -def test_warning_with_setLevel(testdir): - p1 = testdir.makepyfile(""" - def test_inline(caplog): - caplog.setLevel(0) - """) - result = testdir.runpytest_subprocess(p1) - result.stdout.fnmatch_lines([ - "*'caplog.setLevel()' is deprecated," - " use 'caplog.set_level()' instead", - "*1 *warnings*", - ]) From 296ac5c476256cef50bc9a5cc448c34f7d2a389c Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 22 Sep 2017 22:31:29 +0200 Subject: [PATCH 064/159] Add thisch to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index cf31e0389..2fd2ab15e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -166,6 +166,7 @@ Tarcisio Fischer Tareq Alayan Ted Xiao Thomas Grainger +Thomas Hisch Tom Viner Trevor Bekolay Tyler Goodlet From 87596714bf924d2f3ebd23a79ab78a3ad0ea3787 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 27 Sep 2017 00:45:10 +0200 Subject: [PATCH 065/159] minor: cleanup tox.ini --- tox.ini | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tox.ini b/tox.ini index 496005a04..f6dfe82f4 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,6 @@ deps = requests [testenv:py26] -commands = pytest --lsof -ra {posargs:testing} # pinning mock to last supported version for python 2.6 deps = hypothesis<3.0 @@ -117,8 +116,6 @@ commands= pytest -ra {posargs:testing/python/approx.py} [testenv:py27-pluggymaster] -passenv={[testenv]passenv} -commands={[testenv]commands} setenv= _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 deps = @@ -126,8 +123,6 @@ deps = git+https://github.com/pytest-dev/pluggy.git@master [testenv:py35-pluggymaster] -passenv={[testenv:py27-pluggymaster]passenv} -commands={[testenv:py27-pluggymaster]commands} setenv= _PYTEST_SETUP_SKIP_PLUGGY_DEP=1 deps = From 9919269ed048b6e9147ee3301532e3591b9a112b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Sep 2017 02:34:41 -0300 Subject: [PATCH 066/159] Allow to use capsys and capfd in other fixtures Fix #2709 --- _pytest/capture.py | 79 +++++++++++++++++++++++++++++++---------- changelog/2709.bugfix | 1 + testing/test_capture.py | 34 ++++++++++++++++++ 3 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 changelog/2709.bugfix diff --git a/_pytest/capture.py b/_pytest/capture.py index 60f6cd1df..a720e8292 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -61,6 +61,18 @@ def pytest_load_initial_conftests(early_config, parser, args): class CaptureManager: + """ + 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 @@ -88,8 +100,9 @@ class CaptureManager: def resumecapture(self): self._capturing.resume_capturing() - def suspendcapture(self, in_=False): - self.deactivate_funcargs() + def suspendcapture(self, item=None, in_=False): + if item is not None: + self.deactivate_fixture(item) cap = getattr(self, "_capturing", None) if cap is not None: try: @@ -98,16 +111,19 @@ class CaptureManager: cap.suspend_capturing(in_=in_) return outerr - def activate_funcargs(self, pyfuncitem): - capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None) - if capfuncarg is not None: - capfuncarg._start() - self._capfuncarg = capfuncarg + 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_funcargs(self): - capfuncarg = self.__dict__.pop("_capfuncarg", None) - if capfuncarg is not None: - capfuncarg.close() + 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() @pytest.hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector): @@ -126,20 +142,25 @@ class CaptureManager: @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): self.resumecapture() + # no need to activate a capture fixture because they activate themselves during creation; this + # only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will + # be activated during pytest_runtest_call yield self.suspendcapture_item(item, "setup") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): self.resumecapture() - self.activate_funcargs(item) + # it is important to activate this fixture during the call phase so it overwrites the "global" + # capture + self.activate_fixture(item) yield - # self.deactivate_funcargs() called from suspendcapture() self.suspendcapture_item(item, "call") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): self.resumecapture() + self.activate_fixture(item) yield self.suspendcapture_item(item, "teardown") @@ -152,7 +173,7 @@ class CaptureManager: self.reset_capturings() def suspendcapture_item(self, item, when, in_=False): - out, err = self.suspendcapture(in_=in_) + out, err = self.suspendcapture(item, in_=in_) item.add_report_section(when, "stdout", out) item.add_report_section(when, "stderr", err) @@ -168,8 +189,8 @@ def capsys(request): """ if "capfd" in request.fixturenames: raise request.raiseerror(error_capsysfderror) - request.node._capfuncarg = c = CaptureFixture(SysCapture, request) - return c + with _install_capture_fixture_on_item(request, SysCapture) as fixture: + yield fixture @pytest.fixture @@ -181,9 +202,29 @@ def capfd(request): if "capsys" in request.fixturenames: request.raiseerror(error_capsysfderror) if not hasattr(os, 'dup'): - pytest.skip("capfd funcarg needs os.dup") - request.node._capfuncarg = c = CaptureFixture(FDCapture, request) - return c + 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 + + +@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: diff --git a/changelog/2709.bugfix b/changelog/2709.bugfix new file mode 100644 index 000000000..88503b050 --- /dev/null +++ b/changelog/2709.bugfix @@ -0,0 +1 @@ +``capsys`` and ``capfd`` can now be used by other fixtures. diff --git a/testing/test_capture.py b/testing/test_capture.py index eb10f3c07..7e67eaca2 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -517,6 +517,40 @@ class TestCaptureFixture(object): assert 'captured before' not in result.stdout.str() assert 'captured after' not in result.stdout.str() + @pytest.mark.parametrize('fixture', ['capsys', 'capfd']) + def test_fixture_use_by_other_fixtures(self, testdir, fixture): + """ + Ensure that capsys and capfd can be used by other fixtures during setup and teardown. + """ + testdir.makepyfile(""" + from __future__ import print_function + import sys + import pytest + + @pytest.fixture + def captured_print({fixture}): + print('stdout contents begin') + print('stderr contents begin', file=sys.stderr) + out, err = {fixture}.readouterr() + + yield out, err + + print('stdout contents end') + print('stderr contents end', file=sys.stderr) + out, err = {fixture}.readouterr() + assert out == 'stdout contents end\\n' + assert err == 'stderr contents end\\n' + + def test_captured_print(captured_print): + out, err = captured_print + assert out == 'stdout contents begin\\n' + assert err == 'stderr contents begin\\n' + """.format(fixture=fixture)) + result = testdir.runpytest_subprocess() + result.stdout.fnmatch_lines("*1 passed*") + assert 'stdout contents begin' not in result.stdout.str() + assert 'stderr contents begin' not in result.stdout.str() + def test_setup_failure_does_not_kill_capturing(testdir): sub1 = testdir.mkpydir("sub1") From 22f338d74d19e188a5a88a51cc722b771b07c24c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 26 Sep 2017 19:54:26 -0300 Subject: [PATCH 067/159] Refactor some names for better understanding and consistency --- _pytest/capture.py | 61 +++++++++++++++++++++-------------------- _pytest/debugging.py | 4 +-- _pytest/setuponly.py | 4 +-- testing/test_capture.py | 20 +++++++------- 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index a720e8292..ff2a341dc 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -43,7 +43,7 @@ def pytest_load_initial_conftests(early_config, parser, args): pluginmanager.register(capman, "capturemanager") # make sure that capturemanager is properly reset at final shutdown - early_config.add_cleanup(capman.reset_capturings) + early_config.add_cleanup(capman.stop_global_capturing) # make sure logging does not raise exceptions at the end def silence_logging_at_shutdown(): @@ -52,9 +52,9 @@ def pytest_load_initial_conftests(early_config, parser, args): early_config.add_cleanup(silence_logging_at_shutdown) # finally trigger conftest loading but while capturing (issue93) - capman.init_capturings() + capman.start_global_capturing() outcome = yield - out, err = capman.suspendcapture() + out, err = capman.suspend_global_capture() if outcome.excinfo is not None: sys.stdout.write(out) sys.stderr.write(err) @@ -75,6 +75,7 @@ class CaptureManager: def __init__(self, method): self._method = method + self._global_capturing = None def _getcapture(self, method): if method == "fd": @@ -86,24 +87,24 @@ class CaptureManager: else: raise ValueError("unknown capturing method: %r" % method) - def init_capturings(self): - assert not hasattr(self, "_capturing") - self._capturing = self._getcapture(self._method) - self._capturing.start_capturing() + def start_global_capturing(self): + assert self._global_capturing is None + self._global_capturing = self._getcapture(self._method) + self._global_capturing.start_capturing() - def reset_capturings(self): - cap = self.__dict__.pop("_capturing", None) - if cap is not None: - cap.pop_outerr_to_orig() - cap.stop_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 resumecapture(self): - self._capturing.resume_capturing() + def resume_global_capture(self): + self._global_capturing.resume_capturing() - def suspendcapture(self, item=None, in_=False): + def suspend_global_capture(self, item=None, in_=False): if item is not None: self.deactivate_fixture(item) - cap = getattr(self, "_capturing", None) + cap = getattr(self, "_global_capturing", None) if cap is not None: try: outerr = cap.readouterr() @@ -128,9 +129,9 @@ class CaptureManager: @pytest.hookimpl(hookwrapper=True) def pytest_make_collect_report(self, collector): if isinstance(collector, pytest.File): - self.resumecapture() + self.resume_global_capture() outcome = yield - out, err = self.suspendcapture() + out, err = self.suspend_global_capture() rep = outcome.get_result() if out: rep.sections.append(("Captured stdout", out)) @@ -141,39 +142,39 @@ class CaptureManager: @pytest.hookimpl(hookwrapper=True) def pytest_runtest_setup(self, item): - self.resumecapture() + self.resume_global_capture() # no need to activate a capture fixture because they activate themselves during creation; this # only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will # be activated during pytest_runtest_call yield - self.suspendcapture_item(item, "setup") + self.suspend_capture_item(item, "setup") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): - self.resumecapture() + self.resume_global_capture() # it is important to activate this fixture during the call phase so it overwrites the "global" # capture self.activate_fixture(item) yield - self.suspendcapture_item(item, "call") + self.suspend_capture_item(item, "call") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_teardown(self, item): - self.resumecapture() + self.resume_global_capture() self.activate_fixture(item) yield - self.suspendcapture_item(item, "teardown") + self.suspend_capture_item(item, "teardown") @pytest.hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self, excinfo): - self.reset_capturings() + self.stop_global_capturing() @pytest.hookimpl(tryfirst=True) def pytest_internalerror(self, excinfo): - self.reset_capturings() + self.stop_global_capturing() - def suspendcapture_item(self, item, when, in_=False): - out, err = self.suspendcapture(item, in_=in_) + def suspend_capture_item(self, item, when, in_=False): + out, err = self.suspend_global_capture(item, in_=in_) item.add_report_section(when, "stdout", out) item.add_report_section(when, "stderr", err) @@ -252,11 +253,11 @@ class CaptureFixture: @contextlib.contextmanager def disabled(self): capmanager = self.request.config.pluginmanager.getplugin('capturemanager') - capmanager.suspendcapture_item(self.request.node, "call", in_=True) + capmanager.suspend_capture_item(self.request.node, "call", in_=True) try: yield finally: - capmanager.resumecapture() + capmanager.resume_global_capture() def safe_text_dupfile(f, mode, default_encoding="UTF8"): diff --git a/_pytest/debugging.py b/_pytest/debugging.py index aa9c9a386..d7dca7809 100644 --- a/_pytest/debugging.py +++ b/_pytest/debugging.py @@ -54,7 +54,7 @@ class pytestPDB: if cls._pluginmanager is not None: capman = cls._pluginmanager.getplugin("capturemanager") if capman: - capman.suspendcapture(in_=True) + capman.suspend_global_capture(in_=True) tw = _pytest.config.create_terminal_writer(cls._config) tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") @@ -66,7 +66,7 @@ class PdbInvoke: def pytest_exception_interact(self, node, call, report): capman = node.config.pluginmanager.getplugin("capturemanager") if capman: - out, err = capman.suspendcapture(in_=True) + out, err = capman.suspend_global_capture(in_=True) sys.stdout.write(out) sys.stdout.write(err) _enter_pdb(node, call.excinfo, report) diff --git a/_pytest/setuponly.py b/_pytest/setuponly.py index 15e195ad5..a1c7457d7 100644 --- a/_pytest/setuponly.py +++ b/_pytest/setuponly.py @@ -44,7 +44,7 @@ def _show_fixture_action(fixturedef, msg): config = fixturedef._fixturemanager.config capman = config.pluginmanager.getplugin('capturemanager') if capman: - out, err = capman.suspendcapture() + out, err = capman.suspend_global_capture() tw = config.get_terminal_writer() tw.line() @@ -63,7 +63,7 @@ def _show_fixture_action(fixturedef, msg): tw.write('[{0}]'.format(fixturedef.cached_param)) if capman: - capman.resumecapture() + capman.resume_global_capture() sys.stdout.write(out) sys.stderr.write(err) diff --git a/testing/test_capture.py b/testing/test_capture.py index 7e67eaca2..df5fc74f4 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -78,23 +78,23 @@ class TestCaptureManager(object): old = sys.stdout, sys.stderr, sys.stdin try: capman = CaptureManager(method) - capman.init_capturings() - outerr = capman.suspendcapture() + capman.start_global_capturing() + outerr = capman.suspend_global_capture() assert outerr == ("", "") - outerr = capman.suspendcapture() + outerr = capman.suspend_global_capture() assert outerr == ("", "") print("hello") - out, err = capman.suspendcapture() + out, err = capman.suspend_global_capture() if method == "no": assert old == (sys.stdout, sys.stderr, sys.stdin) else: assert not out - capman.resumecapture() + capman.resume_global_capture() print("hello") - out, err = capman.suspendcapture() + out, err = capman.suspend_global_capture() if method != "no": assert out == "hello\n" - capman.reset_capturings() + capman.stop_global_capturing() finally: capouter.stop_capturing() @@ -103,9 +103,9 @@ class TestCaptureManager(object): capouter = StdCaptureFD() try: capman = CaptureManager("fd") - capman.init_capturings() - pytest.raises(AssertionError, "capman.init_capturings()") - capman.reset_capturings() + capman.start_global_capturing() + pytest.raises(AssertionError, "capman.start_global_capturing()") + capman.stop_global_capturing() finally: capouter.stop_capturing() From 3b30c93f73dbda3f96ee5d9ec1c27626bd66a0be Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 27 Sep 2017 16:00:55 -0300 Subject: [PATCH 068/159] Deprecate TerminalReporter._tw Fix #2803 --- _pytest/config.py | 2 +- _pytest/debugging.py | 2 +- _pytest/helpconfig.py | 2 +- _pytest/pastebin.py | 6 +-- _pytest/skipping.py | 4 +- _pytest/terminal.py | 110 +++++++++++++++++++++++------------------ changelog/2803.removal | 1 + 7 files changed, 71 insertions(+), 56 deletions(-) create mode 100644 changelog/2803.removal diff --git a/_pytest/config.py b/_pytest/config.py index 690f98587..2f28bb7bf 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -931,7 +931,7 @@ class Config(object): fslocation=fslocation, nodeid=nodeid)) def get_terminal_writer(self): - return self.pluginmanager.get_plugin("terminalreporter")._tw + return self.pluginmanager.get_plugin("terminalreporter").writer def pytest_cmdline_parse(self, pluginmanager, args): # REF1 assert self == pluginmanager.config, (self, pluginmanager.config) diff --git a/_pytest/debugging.py b/_pytest/debugging.py index aa9c9a386..15a5670e0 100644 --- a/_pytest/debugging.py +++ b/_pytest/debugging.py @@ -83,7 +83,7 @@ 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 = node.config.pluginmanager.getplugin("terminalreporter").writer tw.line() tw.sep(">", "traceback") rep.toterminal(tw) diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index e744637f8..8438c3004 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -107,7 +107,7 @@ def pytest_cmdline_main(config): def showhelp(config): reporter = config.pluginmanager.get_plugin('terminalreporter') - tw = reporter._tw + tw = reporter.writer tw.write(config._parser.optparser.format_help()) tw.line() tw.line() diff --git a/_pytest/pastebin.py b/_pytest/pastebin.py index b588b021b..68aa331f7 100644 --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -25,7 +25,7 @@ def pytest_configure(config): if tr is not None: # pastebin file will be utf-8 encoded binary file config._pastebinfile = tempfile.TemporaryFile('w+b') - oldwrite = tr._tw.write + oldwrite = tr.writer.write def tee_write(s, **kwargs): oldwrite(s, **kwargs) @@ -33,7 +33,7 @@ def pytest_configure(config): s = s.encode('utf-8') config._pastebinfile.write(s) - tr._tw.write = tee_write + tr.writer.write = tee_write def pytest_unconfigure(config): @@ -45,7 +45,7 @@ def pytest_unconfigure(config): del config._pastebinfile # undo our patching in the terminal reporter tr = config.pluginmanager.getplugin('terminalreporter') - del tr._tw.__dict__['write'] + del tr.writer.__dict__['write'] # write summary tr.write_sep("=", "Sending information to Paste Service") pastebinurl = create_new_paste(sessionlog) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 2b5d0dded..ef9f601ca 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -295,9 +295,9 @@ def pytest_terminal_summary(terminalreporter): show_simple(terminalreporter, lines, 'passed', "PASSED %s") if lines: - tr._tw.sep("=", "short test summary info") + tr.writer.sep("=", "short test summary info") for line in lines: - tr._tw.line(line) + tr.writer.line(line) def show_simple(terminalreporter, lines, stat, format): diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 5dd8ac940..92f319766 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -5,15 +5,18 @@ This is a good source for looking at the various reporting hooks. from __future__ import absolute_import, division, print_function import itertools -from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ - EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED -import pytest -import py -import six +import platform import sys import time -import platform +import warnings + +import py +import six + import pluggy +import pytest +from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ + EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED def pytest_addoption(parser): @@ -136,13 +139,22 @@ class TerminalReporter: self.startdir = py.path.local() if file is None: file = sys.stdout - self._tw = self.writer = _pytest.config.create_terminal_writer(config, - file) + self._writer = _pytest.config.create_terminal_writer(config, file) self.currentfspath = None self.reportchars = getreportopt(config) - self.hasmarkup = self._tw.hasmarkup + self.hasmarkup = self.writer.hasmarkup self.isatty = file.isatty() + @property + def writer(self): + return self._writer + + @property + def _tw(self): + warnings.warn(DeprecationWarning('TerminalReporter._tw is deprecated, use TerminalReporter.writer instead'), + stacklevel=2) + return self.writer + def hasopt(self, char): char = {'xfailed': 'x', 'skipped': 's'}.get(char, char) return char in self.reportchars @@ -152,32 +164,32 @@ class TerminalReporter: if fspath != self.currentfspath: self.currentfspath = fspath fspath = self.startdir.bestrelpath(fspath) - self._tw.line() - self._tw.write(fspath + " ") - self._tw.write(res) + self.writer.line() + self.writer.write(fspath + " ") + self.writer.write(res) def write_ensure_prefix(self, prefix, extra="", **kwargs): if self.currentfspath != prefix: - self._tw.line() + self.writer.line() self.currentfspath = prefix - self._tw.write(prefix) + self.writer.write(prefix) if extra: - self._tw.write(extra, **kwargs) + self.writer.write(extra, **kwargs) self.currentfspath = -2 def ensure_newline(self): if self.currentfspath: - self._tw.line() + self.writer.line() self.currentfspath = None def write(self, content, **markup): - self._tw.write(content, **markup) + self.writer.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) + self.writer.line(line, **markup) def rewrite(self, line, **markup): """ @@ -190,22 +202,22 @@ class TerminalReporter: """ erase = markup.pop('erase', False) if erase: - fill_count = self._tw.fullwidth - len(line) + fill_count = self.writer.fullwidth - len(line) fill = ' ' * fill_count else: fill = '' line = str(line) - self._tw.write("\r" + line + fill, **markup) + self.writer.write("\r" + line + fill, **markup) def write_sep(self, sep, title=None, **markup): self.ensure_newline() - self._tw.sep(sep, title, **markup) + self.writer.sep(sep, title, **markup) def section(self, title, sep="=", **kw): - self._tw.sep(sep, title, **kw) + self.writer.sep(sep, title, **kw) def line(self, msg, **kw): - self._tw.line(msg, **kw) + self.writer.line(msg, **kw) def pytest_internalerror(self, excrepr): for line in six.text_type(excrepr).split("\n"): @@ -252,7 +264,7 @@ class TerminalReporter: if not hasattr(rep, 'node') and self.showfspath: self.write_fspath_result(rep.nodeid, letter) else: - self._tw.write(letter) + self.writer.write(letter) else: if isinstance(word, tuple): word, markup = word @@ -263,16 +275,18 @@ class TerminalReporter: markup = {'red': True} elif rep.skipped: markup = {'yellow': True} + else: + markup = {} line = self._locationline(rep.nodeid, *rep.location) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) - # self._tw.write(word, **markup) + # self.writer.write(word, **markup) else: self.ensure_newline() if hasattr(rep, 'node'): - self._tw.write("[%s] " % rep.node.gateway.id) - self._tw.write(word, **markup) - self._tw.write(" " + line) + self.writer.write("[%s] " % rep.node.gateway.id) + self.writer.write(word, **markup) + self.writer.write(" " + line) self.currentfspath = -2 def pytest_collection(self): @@ -358,9 +372,9 @@ class TerminalReporter: if self.config.option.collectonly: self._printcollecteditems(session.items) if self.stats.get('failed'): - self._tw.sep("!", "collection failures") + self.writer.sep("!", "collection failures") for rep in self.stats.get('failed'): - rep.toterminal(self._tw) + rep.toterminal(self.writer) return 1 return 0 lines = self.config.hook.pytest_report_collectionfinish( @@ -378,12 +392,12 @@ class TerminalReporter: 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)) + self.writer.line("%s: %d" % (name, count)) else: for item in items: nodeid = item.nodeid nodeid = nodeid.replace("::()::", "::") - self._tw.line(nodeid) + self.writer.line(nodeid) return stack = [] indent = "" @@ -398,13 +412,13 @@ class TerminalReporter: # if col.name == "()": # continue indent = (len(stack) - 1) * " " - self._tw.line("%s%s" % (indent, col)) + self.writer.line("%s%s" % (indent, col)) @pytest.hookimpl(hookwrapper=True) def pytest_sessionfinish(self, exitstatus): outcome = yield outcome.get_result() - self._tw.line("") + self.writer.line("") summary_exit_codes = ( EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED) @@ -434,10 +448,10 @@ class TerminalReporter: self.write_sep("!", msg) if "KeyboardInterrupt" in msg: if self.config.option.fulltrace: - excrepr.toterminal(self._tw) + excrepr.toterminal(self.writer) else: - self._tw.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True) - excrepr.reprcrash.toterminal(self._tw) + self.writer.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True) + excrepr.reprcrash.toterminal(self.writer) def _locationline(self, nodeid, fspath, lineno, domain): def mkrel(nodeid): @@ -493,14 +507,14 @@ class TerminalReporter: grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config)) self.write_sep("=", "warnings summary", yellow=True, bold=False) - for location, warnings in grouped: - self._tw.line(str(location) or '') - for w in warnings: + for location, warning_records in grouped: + self.writer.line(str(location) or '') + for w in warning_records: lines = w.message.splitlines() indented = '\n'.join(' ' + x for x in lines) - self._tw.line(indented) - self._tw.line() - self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html') + self.writer.line(indented) + self.writer.line() + self.writer.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html') def summary_passes(self): if self.config.option.tbstyle != "no": @@ -517,10 +531,10 @@ class TerminalReporter: def print_teardown_sections(self, rep): for secname, content in rep.sections: if 'teardown' in secname: - self._tw.sep('-', secname) + self.writer.sep('-', secname) if content[-1:] == "\n": content = content[:-1] - self._tw.line(content) + self.writer.line(content) def summary_failures(self): if self.config.option.tbstyle != "no": @@ -560,12 +574,12 @@ class TerminalReporter: self._outrep_summary(rep) def _outrep_summary(self, rep): - rep.toterminal(self._tw) + rep.toterminal(self.writer) for secname, content in rep.sections: - self._tw.sep("-", secname) + self.writer.sep("-", secname) if content[-1:] == "\n": content = content[:-1] - self._tw.line(content) + self.writer.line(content) def summary_stats(self): session_duration = time.time() - self._sessionstarttime diff --git a/changelog/2803.removal b/changelog/2803.removal new file mode 100644 index 000000000..4ebdb903e --- /dev/null +++ b/changelog/2803.removal @@ -0,0 +1 @@ +``TerminalReporter._tw`` has been deprecated in favor of ``TerminalReporter.writer`` and will be removed in a future version. Also, ``TerminalReporter.writer`` is now read-only. From f9589f7b6487062474ba7a6af583e3360c2e6bae Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 29 Sep 2017 17:24:31 -0300 Subject: [PATCH 069/159] Resume output capturing after capsys/capfd.disabled() context manager Fix #1993 --- _pytest/capture.py | 4 +++- changelog/1993.bugfix | 1 + testing/test_capture.py | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 changelog/1993.bugfix diff --git a/_pytest/capture.py b/_pytest/capture.py index ff2a341dc..13e1216cc 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -252,12 +252,14 @@ class CaptureFixture: @contextlib.contextmanager def disabled(self): + self._capture.suspend_capturing() capmanager = self.request.config.pluginmanager.getplugin('capturemanager') - capmanager.suspend_capture_item(self.request.node, "call", in_=True) + capmanager.suspend_global_capture(item=None, in_=False) try: yield finally: capmanager.resume_global_capture() + self._capture.resume_capturing() def safe_text_dupfile(f, mode, default_encoding="UTF8"): diff --git a/changelog/1993.bugfix b/changelog/1993.bugfix new file mode 100644 index 000000000..07a78cc91 --- /dev/null +++ b/changelog/1993.bugfix @@ -0,0 +1 @@ +Resume output capturing after ``capsys/capfd.disabled()`` context manager. diff --git a/testing/test_capture.py b/testing/test_capture.py index df5fc74f4..0fd012f7b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -502,20 +502,30 @@ class TestCaptureFixture(object): assert 'closed' not in result.stderr.str() @pytest.mark.parametrize('fixture', ['capsys', 'capfd']) - def test_disabled_capture_fixture(self, testdir, fixture): + @pytest.mark.parametrize('no_capture', [True, False]) + def test_disabled_capture_fixture(self, testdir, fixture, no_capture): testdir.makepyfile(""" def test_disabled({fixture}): print('captured before') with {fixture}.disabled(): print('while capture is disabled') print('captured after') + assert {fixture}.readouterr() == ('captured before\\ncaptured after\\n', '') + + def test_normal(): + print('test_normal executed') """.format(fixture=fixture)) - result = testdir.runpytest_subprocess() + args = ('-s',) if no_capture else () + result = testdir.runpytest_subprocess(*args) result.stdout.fnmatch_lines(""" *while capture is disabled* """) assert 'captured before' not in result.stdout.str() assert 'captured after' not in result.stdout.str() + if no_capture: + assert 'test_normal executed' in result.stdout.str() + else: + assert 'test_normal executed' not in result.stdout.str() @pytest.mark.parametrize('fixture', ['capsys', 'capfd']) def test_fixture_use_by_other_fixtures(self, testdir, fixture): From 79d33530812748482726a82a45b0e2754928c756 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Sun, 1 Oct 2017 18:38:29 -0300 Subject: [PATCH 070/159] Add allow_module_level kwarg to skip helper --- _pytest/outcomes.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/_pytest/outcomes.py b/_pytest/outcomes.py index ff5ef756d..75b0250f4 100644 --- a/_pytest/outcomes.py +++ b/_pytest/outcomes.py @@ -62,14 +62,21 @@ def exit(msg): exit.Exception = Exit -def skip(msg=""): +def skip(msg="", **kwargs): """ skip an executing test with the given message. Note: it's usually better to use the pytest.mark.skipif marker to declare a test to be skipped under certain conditions like mismatching platforms or dependencies. See the pytest_skipping plugin for details. + + :kwarg bool allow_module_level: allows this function to be called at + module level, skipping the rest of the module. Default to False. """ __tracebackhide__ = True - raise Skipped(msg=msg) + allow_module_level = kwargs.pop('allow_module_level', False) + if kwargs: + keys = [k for k in kwargs.keys()] + raise TypeError('unexpected keyworkd arguments: {}'.format(keys)) + raise Skipped(msg=msg, allow_module_level=allow_module_level) skip.Exception = Skipped From 06307be15d2db8d327c5f6f82d07aa590b760472 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Sun, 1 Oct 2017 18:39:14 -0300 Subject: [PATCH 071/159] Add initial tests using skip with allow_module_level kwarg --- testing/test_skipping.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 1fbb9ed0f..23c4c37ce 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1005,6 +1005,40 @@ def test_module_level_skip_error(testdir): ) +def test_module_level_skip_with_allow_module_level(testdir): + """ + Verify that using pytest.skip(allow_module_level=True) is allowed + """ + testdir.makepyfile(""" + import pytest + pytest.skip("skip_module_level", allow_module_level=True) + + def test_func(): + assert 0 + """) + result = testdir.runpytest("-rxs") + result.stdout.fnmatch_lines( + "*SKIP*skip_module_level" + ) + + +def test_invalid_skip_keyword_parameter(testdir): + """ + Verify that using pytest.skip() with unknown parameter raises an error + """ + testdir.makepyfile(""" + import pytest + pytest.skip("skip_module_level", unknown=1) + + def test_func(): + assert 0 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines( + "*TypeError:*['unknown']*" + ) + + def test_mark_xfail_item(testdir): # Ensure pytest.mark.xfail works with non-Python Item testdir.makeconftest(""" From e4a6e52b81c96bbeab2ff5506994722408622fd1 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Sun, 1 Oct 2017 18:40:19 -0300 Subject: [PATCH 072/159] Update skipping documentation to include usage of allow_module_level kwarg --- doc/en/skipping.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 630f73422..e6f1bc3c5 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -54,6 +54,15 @@ by calling the ``pytest.skip(reason)`` function: if not valid_config(): pytest.skip("unsupported configuration") +It is also possible to skip the whole module using +``pytest.skip(reason, allow_module_level=True)`` at the module level: + + +.. code-block:: python + + if not enabled_platform_edge_cases(): + pytest.skip("unsupported platform", allow_module_level=True) + The imperative method is useful when it is not possible to evaluate the skip condition during import time. From c1aa63c0bbfe9b0b0613617b2c98941ff487cd03 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Mon, 2 Oct 2017 21:24:52 -0300 Subject: [PATCH 073/159] Fix docstring alignment and typos --- _pytest/outcomes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/outcomes.py b/_pytest/outcomes.py index 75b0250f4..7f0c18fa6 100644 --- a/_pytest/outcomes.py +++ b/_pytest/outcomes.py @@ -69,13 +69,13 @@ def skip(msg="", **kwargs): dependencies. See the pytest_skipping plugin for details. :kwarg bool allow_module_level: allows this function to be called at - module level, skipping the rest of the module. Default to False. + module level, skipping the rest of the module. Default to False. """ __tracebackhide__ = True allow_module_level = kwargs.pop('allow_module_level', False) if kwargs: keys = [k for k in kwargs.keys()] - raise TypeError('unexpected keyworkd arguments: {}'.format(keys)) + raise TypeError('unexpected keyword arguments: {0}'.format(keys)) raise Skipped(msg=msg, allow_module_level=allow_module_level) From 59f66933cd9468eec3dc67714f1b686f8dfb1c39 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Mon, 2 Oct 2017 21:26:00 -0300 Subject: [PATCH 074/159] Update documentation example of pytest.skip(allow_module_level=True) --- doc/en/skipping.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index e6f1bc3c5..2c91bab71 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -57,11 +57,12 @@ by calling the ``pytest.skip(reason)`` function: It is also possible to skip the whole module using ``pytest.skip(reason, allow_module_level=True)`` at the module level: - .. code-block:: python - if not enabled_platform_edge_cases(): - pytest.skip("unsupported platform", allow_module_level=True) + import pytest + + if not pytest.config.getoption("--custom-flag"): + pytest.skip("--custom-flag is missing, skipping tests", allow_module_level=True) The imperative method is useful when it is not possible to evaluate the skip condition during import time. From 9824499396a16cea957f1cd87c2c245b1cd68d8a Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Mon, 2 Oct 2017 21:26:29 -0300 Subject: [PATCH 075/159] Add 2808.feature changelog entry --- changelog/2808.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2808.feature diff --git a/changelog/2808.feature b/changelog/2808.feature new file mode 100644 index 000000000..26245f047 --- /dev/null +++ b/changelog/2808.feature @@ -0,0 +1 @@ +Add ``allow_module_level`` kwarg to ``pytest.skip()``, enabling to skip the whole module. From fbb9e9328bbc008be02ad050d5703a44c90e46d2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 2 Oct 2017 16:20:51 -0300 Subject: [PATCH 076/159] Fix warning about non-ascii warnings even when they are ascii Fix #2809 --- _pytest/warnings.py | 4 ++-- changelog/2809.bugfix | 1 + testing/test_warnings.py | 22 +++++++++++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 changelog/2809.bugfix diff --git a/_pytest/warnings.py b/_pytest/warnings.py index 926b1f581..4a4b6e687 100644 --- a/_pytest/warnings.py +++ b/_pytest/warnings.py @@ -72,8 +72,8 @@ def catch_warnings_for_item(item): unicode_warning = False if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): - new_args = [compat.safe_str(m) for m in warn_msg.args] - unicode_warning = warn_msg.args != new_args + new_args = [m.encode('ascii', 'replace') for m in warn_msg.args] + unicode_warning = list(warn_msg.args) != new_args warn_msg.args = new_args msg = warnings.formatwarning( diff --git a/changelog/2809.bugfix b/changelog/2809.bugfix new file mode 100644 index 000000000..6db7e8c6c --- /dev/null +++ b/changelog/2809.bugfix @@ -0,0 +1 @@ +Pytest no longer complains about warnings with unicode messages being non-ascii compatible even for ascii-compatible messages. As a result of this, warnings with unicode messages are converted first to an ascii representation for safety. \ No newline at end of file diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 1328cc3f2..fea3959f9 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -163,13 +163,33 @@ def test_py2_unicode(testdir, pyfile_with_warnings): result.stdout.fnmatch_lines([ '*== %s ==*' % WARNINGS_SUMMARY_HEADER, - '*test_py2_unicode.py:8: UserWarning: \u6d4b\u8bd5', + '*test_py2_unicode.py:8: UserWarning: ??', '*warnings.warn(u"\u6d4b\u8bd5")', '*warnings.py:*: UnicodeWarning: Warning is using unicode non*', '* 1 passed, 2 warnings*', ]) +def test_py2_unicode_ascii(testdir): + """Ensure that our warning about 'unicode warnings containing non-ascii messages' + does not trigger with ascii-convertible messages""" + testdir.makeini('[pytest]') + testdir.makepyfile(''' + import pytest + import warnings + + @pytest.mark.filterwarnings('always') + def test_func(): + warnings.warn(u"hello") + ''') + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*== %s ==*' % WARNINGS_SUMMARY_HEADER, + '*warnings.warn(u"hello")', + '* 1 passed, 1 warnings in*' + ]) + + def test_works_with_filterwarnings(testdir): """Ensure our warnings capture does not mess with pre-installed filters (#2430).""" testdir.makepyfile(''' From df6d5cd4e7df94f276281a48826202b622c0a68d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 3 Oct 2017 11:09:24 -0300 Subject: [PATCH 077/159] Use ascii_escaped to escape unicode warnings --- _pytest/compat.py | 4 ++-- _pytest/python.py | 10 +++++----- _pytest/warnings.py | 2 +- testing/test_warnings.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/_pytest/compat.py b/_pytest/compat.py index 45491a722..7bf3bb9b8 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -149,7 +149,7 @@ if _PY3: # empty bytes crashes codecs.escape_encode (#1087) return '' - def _ascii_escaped(val): + def ascii_escaped(val): """If val is pure ascii, returns it as a str(). Otherwise, escapes bytes objects into a sequence of escaped bytes: @@ -177,7 +177,7 @@ else: from itertools import imap, izip # NOQA - def _ascii_escaped(val): + 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. diff --git a/_pytest/python.py b/_pytest/python.py index e3d79b2e9..0161c10e8 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -19,7 +19,7 @@ import pluggy from _pytest import fixtures from _pytest import main from _pytest.compat import ( - isclass, isfunction, is_generator, _ascii_escaped, + isclass, isfunction, is_generator, ascii_escaped, REGEX_TYPE, STRING_TYPES, NoneType, NOTSET, get_real_func, getfslineno, safe_getattr, safe_str, getlocation, enum, @@ -922,7 +922,7 @@ def _idval(val, argname, idx, idfn, config=None): msg += '\nUpdate your code as this will raise an error in pytest-4.0.' warnings.warn(msg, DeprecationWarning) if s: - return _ascii_escaped(s) + return ascii_escaped(s) if config: hook_id = config.hook.pytest_make_parametrize_id( @@ -931,11 +931,11 @@ def _idval(val, argname, idx, idfn, config=None): return hook_id if isinstance(val, STRING_TYPES): - return _ascii_escaped(val) + return ascii_escaped(val) elif isinstance(val, (float, int, bool, NoneType)): return str(val) elif isinstance(val, REGEX_TYPE): - return _ascii_escaped(val.pattern) + return ascii_escaped(val.pattern) elif enum is not None and isinstance(val, enum.Enum): return str(val) elif isclass(val) and hasattr(val, '__name__'): @@ -951,7 +951,7 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None): for val, argname in zip(parameterset.values, argnames)] return "-".join(this_id) else: - return _ascii_escaped(ids[idx]) + return ascii_escaped(ids[idx]) def idmaker(argnames, parametersets, idfn=None, ids=None, config=None): diff --git a/_pytest/warnings.py b/_pytest/warnings.py index 4a4b6e687..847771daa 100644 --- a/_pytest/warnings.py +++ b/_pytest/warnings.py @@ -72,7 +72,7 @@ def catch_warnings_for_item(item): unicode_warning = False if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): - new_args = [m.encode('ascii', 'replace') for m in warn_msg.args] + new_args = [compat.ascii_escaped(m) for m in warn_msg.args] unicode_warning = list(warn_msg.args) != new_args warn_msg.args = new_args diff --git a/testing/test_warnings.py b/testing/test_warnings.py index fea3959f9..4beff4548 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -163,7 +163,7 @@ def test_py2_unicode(testdir, pyfile_with_warnings): result.stdout.fnmatch_lines([ '*== %s ==*' % WARNINGS_SUMMARY_HEADER, - '*test_py2_unicode.py:8: UserWarning: ??', + '*test_py2_unicode.py:8: UserWarning: \\u6d4b\\u8bd5', '*warnings.warn(u"\u6d4b\u8bd5")', '*warnings.py:*: UnicodeWarning: Warning is using unicode non*', '* 1 passed, 2 warnings*', From 03ce0adb79b012a6a34adb2d2fe6a6d9ba9b6fb2 Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Wed, 4 Oct 2017 19:18:55 -0300 Subject: [PATCH 078/159] Fix: handle CollectReport in folded_skips function --- _pytest/skipping.py | 3 ++- testing/test_skipping.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index ef9f601ca..0fe602972 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -349,7 +349,8 @@ def folded_skips(skipped): # folding reports with global pytestmark variable # this is workaround, because for now we cannot identify the scope of a skip marker # TODO: revisit after marks scope would be fixed - if event.when == 'setup' and 'skip' in keywords and 'pytestmark' not in keywords: + when = getattr(event, 'when', None) + if when == 'setup' and 'skip' in keywords and 'pytestmark' not in keywords: key = (key[0], None, key[2], ) d.setdefault(key, []).append(event) l = [] diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 23c4c37ce..ae334e7cd 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -663,7 +663,7 @@ def test_skipif_class(testdir): def test_skip_reasons_folding(): - path = 'xyz' + path = "xyz" lineno = 3 message = "justso" longrepr = (path, lineno, message) @@ -680,10 +680,15 @@ def test_skip_reasons_folding(): ev2.longrepr = longrepr ev2.skipped = True - l = folded_skips([ev1, ev2]) + # ev3 might be a collection report + ev3 = X() + ev3.longrepr = longrepr + ev3.skipped = True + + l = folded_skips([ev1, ev2, ev3]) assert len(l) == 1 num, fspath, lineno, reason = l[0] - assert num == 2 + assert num == 3 assert fspath == path assert lineno == lineno assert reason == message From 0668a6c6d3aee3d52239165fd6f7f8a2bb5d6f9d Mon Sep 17 00:00:00 2001 From: "George Y. Kussumoto" Date: Wed, 4 Oct 2017 22:14:29 -0300 Subject: [PATCH 079/159] Add myself to authors file --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index cf31e0389..0fb9c7e04 100644 --- a/AUTHORS +++ b/AUTHORS @@ -64,6 +64,7 @@ Feng Ma Florian Bruhin Floris Bruynooghe Gabriel Reis +George Kussumoto Georgy Dyuldin Graham Horler Greg Price From 667e70f5551da23b86dd6ee29b5f77d5c604d003 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Sep 2017 12:17:08 +0200 Subject: [PATCH 080/159] switch out the placeholder MarkEvaluator in unittest plugin --- _pytest/unittest.py | 3 +-- changelog/2767.trivial | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelog/2767.trivial diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 585f81472..5c7f38f48 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -134,8 +134,7 @@ class TestCaseFunction(Function): try: skip(reason) except skip.Exception: - self._evalskip = MarkEvaluator(self, 'SkipTest') - self._evalskip.result = True + self._evalskip = True self._addexcinfo(sys.exc_info()) def addExpectedFailure(self, testcase, rawexcinfo, reason=""): diff --git a/changelog/2767.trivial b/changelog/2767.trivial new file mode 100644 index 000000000..c42a06e07 --- /dev/null +++ b/changelog/2767.trivial @@ -0,0 +1 @@ +* remove unnecessary mark evaluator in unittest plugin \ No newline at end of file From a33650953a4838cd742f547ea14dcd8fc7731adb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Sep 2017 12:20:29 +0200 Subject: [PATCH 081/159] remove unused import --- _pytest/unittest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 5c7f38f48..1ea1b0121 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -9,7 +9,6 @@ import _pytest._code from _pytest.config import hookimpl from _pytest.outcomes import fail, skip, xfail from _pytest.python import transfer_markers, Class, Module, Function -from _pytest.skipping import MarkEvaluator def pytest_pycollect_makeitem(collector, name, obj): From 9ad2b75038204ab44125219797df3b9dabb99889 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 13 Sep 2017 12:49:11 +0200 Subject: [PATCH 082/159] skipping: replace _evalskip with a more consistent _skipped_by_mark --- _pytest/skipping.py | 9 ++++----- _pytest/unittest.py | 2 +- changelog/2767.removal | 2 ++ 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelog/2767.removal diff --git a/_pytest/skipping.py b/_pytest/skipping.py index ef9f601ca..e2565f8ae 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -155,17 +155,17 @@ class MarkEvaluator: @hookimpl(tryfirst=True) def pytest_runtest_setup(item): # Check if skip or skipif are specified as pytest marks - + item._skipped_by_mark = False skipif_info = item.keywords.get('skipif') if isinstance(skipif_info, (MarkInfo, MarkDecorator)): eval_skipif = MarkEvaluator(item, 'skipif') if eval_skipif.istrue(): - item._evalskip = eval_skipif + item._skipped_by_mark = True skip(eval_skipif.getexplanation()) skip_info = item.keywords.get('skip') if isinstance(skip_info, (MarkInfo, MarkDecorator)): - item._evalskip = True + item._skipped_by_mark = True if 'reason' in skip_info.kwargs: skip(skip_info.kwargs['reason']) elif skip_info.args: @@ -212,7 +212,6 @@ def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() evalxfail = getattr(item, '_evalxfail', None) - evalskip = getattr(item, '_evalskip', 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 @@ -248,7 +247,7 @@ def pytest_runtest_makereport(item, call): else: rep.outcome = "passed" rep.wasxfail = explanation - elif evalskip is not None and rep.skipped and type(rep.longrepr) is tuple: + elif item._skipped_by_mark 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 diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 1ea1b0121..7842d1658 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -133,7 +133,7 @@ class TestCaseFunction(Function): try: skip(reason) except skip.Exception: - self._evalskip = True + self._skipped_by_mark = True self._addexcinfo(sys.exc_info()) def addExpectedFailure(self, testcase, rawexcinfo, reason=""): diff --git a/changelog/2767.removal b/changelog/2767.removal new file mode 100644 index 000000000..702a0a36c --- /dev/null +++ b/changelog/2767.removal @@ -0,0 +1,2 @@ +* remove the internal optional multi-typed attribute _evalskip + and replacce it with the boolean _skipped_by_mark \ No newline at end of file From 8480075f01af2f35e97ad99bb6eeb39b90b0a24a Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 14 Sep 2017 09:31:07 +0200 Subject: [PATCH 083/159] resuffle markevaluator internal structure --- _pytest/skipping.py | 98 +++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index e2565f8ae..838cee563 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -60,22 +60,31 @@ def pytest_configure(config): ) -class MarkEvaluator: +class MarkEvaluator(object): def __init__(self, item, name): self.item = item - self.name = name - - @property - def holder(self): - return self.item.keywords.get(self.name) + self._marks = None + self._mark = None + self._repr_name = name def __bool__(self): - return bool(self.holder) + self._marks = self._get_marks() + return bool(self._marks) __nonzero__ = __bool__ def wasvalid(self): return not hasattr(self, 'exc') + def _get_marks(self): + + keyword = self.item.keywords.get(self._repr_name) + if isinstance(keyword, MarkDecorator): + return [keyword.mark] + elif isinstance(keyword, MarkInfo): + return [x.combined for x in keyword] + else: + return [] + def invalidraise(self, exc): raises = self.get('raises') if not raises: @@ -95,7 +104,7 @@ class MarkEvaluator: fail("Error evaluating %r expression\n" " %s\n" "%s" - % (self.name, self.expr, "\n".join(msg)), + % (self._repr_name, self.expr, "\n".join(msg)), pytrace=False) def _getglobals(self): @@ -107,40 +116,51 @@ class MarkEvaluator: def _istrue(self): if hasattr(self, 'result'): return self.result - if self.holder: - if self.holder.args or 'condition' in self.holder.kwargs: - self.result = False - # "holder" might be a MarkInfo or a MarkDecorator; only - # MarkInfo keeps track of all parameters it received in an - # _arglist attribute - marks = getattr(self.holder, '_marks', None) \ - or [self.holder.mark] - for _, args, kwargs in marks: - if 'condition' in kwargs: - args = (kwargs['condition'],) - for expr in args: + self._marks = self._get_marks() + + def needs_eval(mark): + return mark.args or 'condition' in mark.kwargs + + if self._marks: + self.result = False + # "holder" might be a MarkInfo or a MarkDecorator; only + # MarkInfo keeps track of all parameters it received in an + # _arglist attribute + 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 - if isinstance(expr, six.string_types): - d = self._getglobals() - result = cached_eval(self.item.config, expr, d) - else: - if "reason" not in 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 = kwargs.get('reason', None) - self.expr = expr - return self.result - else: - self.result = True - return getattr(self, 'result', False) + 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): - return self.holder.kwargs.get(attr, default) + 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) From e3b73682b229fc52db2124981bd6f21ad1c2dc9d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 15 Sep 2017 08:47:20 +0200 Subject: [PATCH 084/159] flake8 fix --- _pytest/skipping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 838cee563..e439105d4 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -151,7 +151,7 @@ class MarkEvaluator(object): self.expr = expr return self.result - if not args: + if not args: self.result = True self.reason = mark.kwargs.get('reason', None) return self.result From 459cc401929db9f6c8b9970a015ab251b221ffec Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 16 Sep 2017 21:08:48 +0200 Subject: [PATCH 085/159] skipping: cleanup remove dead comments fix naming remove dead code --- _pytest/skipping.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index e439105d4..11ae2d03d 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -65,7 +65,7 @@ class MarkEvaluator(object): self.item = item self._marks = None self._mark = None - self._repr_name = name + self._mark_name = name def __bool__(self): self._marks = self._get_marks() @@ -77,7 +77,7 @@ class MarkEvaluator(object): def _get_marks(self): - keyword = self.item.keywords.get(self._repr_name) + keyword = self.item.keywords.get(self._mark_name) if isinstance(keyword, MarkDecorator): return [keyword.mark] elif isinstance(keyword, MarkInfo): @@ -104,7 +104,7 @@ class MarkEvaluator(object): fail("Error evaluating %r expression\n" " %s\n" "%s" - % (self._repr_name, self.expr, "\n".join(msg)), + % (self._mark_name, self.expr, "\n".join(msg)), pytrace=False) def _getglobals(self): @@ -118,14 +118,8 @@ class MarkEvaluator(object): return self.result self._marks = self._get_marks() - def needs_eval(mark): - return mark.args or 'condition' in mark.kwargs - if self._marks: self.result = False - # "holder" might be a MarkInfo or a MarkDecorator; only - # MarkInfo keeps track of all parameters it received in an - # _arglist attribute for mark in self._marks: self._mark = mark if 'condition' in mark.kwargs: From 8a6bdb282f2ddaa72f5010e5fbe88726b4b8d022 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 9 Oct 2017 15:21:06 +0200 Subject: [PATCH 086/159] fix changelog entry --- changelog/2767.removal | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog/2767.removal b/changelog/2767.removal index 702a0a36c..b9c3984cd 100644 --- a/changelog/2767.removal +++ b/changelog/2767.removal @@ -1,2 +1 @@ -* remove the internal optional multi-typed attribute _evalskip - and replacce it with the boolean _skipped_by_mark \ No newline at end of file +Remove the internal multi-typed attribute ``Node._evalskip`` and replace it with the boolean ``Node._skipped_by_mark``. \ No newline at end of file From 88366b393ce1501ac73351239838fd5fa708c115 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 9 Oct 2017 16:35:33 +0200 Subject: [PATCH 087/159] start the removal of python 2.6/3.3 support --- changelog/2812.removal | 1 + setup.py | 17 +++++++++-------- tox.ini | 12 ++---------- 3 files changed, 12 insertions(+), 18 deletions(-) create mode 100644 changelog/2812.removal diff --git a/changelog/2812.removal b/changelog/2812.removal new file mode 100644 index 000000000..c619ee2da --- /dev/null +++ b/changelog/2812.removal @@ -0,0 +1 @@ +remove support for the eol python versions 2.6 and 3.3 \ No newline at end of file diff --git a/setup.py b/setup.py index 4d74e6bca..b58a4014c 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ classifiers = [ 'Topic :: Utilities', ] + [ ('Programming Language :: Python :: %s' % x) - for x in '2 2.6 2.7 3 3.3 3.4 3.5 3.6'.split() + for x in '2.7 3 3.4 3.5 3.6'.split() ] with open('README.rst') as fd: @@ -50,12 +50,8 @@ def main(): install_requires.append('pluggy>=0.4.0,<0.5') extras_require = {} if has_environment_marker_support(): - extras_require[':python_version=="2.6"'] = ['argparse', 'ordereddict'] extras_require[':sys_platform=="win32"'] = ['colorama'] else: - if sys.version_info < (2, 7): - install_requires.append('argparse') - install_requires.append('ordereddict') if sys.platform == 'win32': install_requires.append('colorama') @@ -69,9 +65,11 @@ def main(): url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], - author='Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others', - entry_points={'console_scripts': - ['pytest=pytest:main', 'py.test=pytest:main']}, + author=( + 'Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, ' + 'Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others'), + entry_points={'console_scripts': [ + 'pytest=pytest:main', 'py.test=pytest:main']}, classifiers=classifiers, keywords="test unittest", cmdclass={'test': PyTest}, @@ -87,10 +85,13 @@ def main(): class PyTest(Command): user_options = [] + def initialize_options(self): pass + def finalize_options(self): pass + def run(self): import subprocess PPATH = [x for x in os.environ.get('PYTHONPATH', '').split(':') if x] diff --git a/tox.ini b/tox.ini index f6dfe82f4..0f9611e7d 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,6 @@ distshare = {homedir}/.tox/distshare # make sure to update environment list in travis.yml and appveyor.yml envlist = linting - py26 py27 py33 py34 @@ -27,13 +26,6 @@ deps = mock requests -[testenv:py26] -# pinning mock to last supported version for python 2.6 -deps = - hypothesis<3.0 - nose - mock<1.1 - [testenv:py27-subprocess] changedir = . deps = @@ -54,7 +46,7 @@ deps = pygments restructuredtext_lint commands = - flake8 pytest.py _pytest testing + flake8 pytest.py _pytest testing setup.py pytest.py {envpython} scripts/check-rst.py [testenv:py27-xdist] @@ -174,7 +166,7 @@ usedevelop = True deps = autopep8 commands = - autopep8 --in-place -r --max-line-length=120 --exclude=test_source_multiline_block.py _pytest testing + autopep8 --in-place -r --max-line-length=120 --exclude=test_source_multiline_block.py _pytest testing setup.py pytest.py [testenv:jython] changedir = testing From 73ff53c742de501d0f3b4c4d6e3cdaa6a8b5fde5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 9 Oct 2017 16:36:41 +0200 Subject: [PATCH 088/159] remove eol python from the ci config --- .travis.yml | 4 ---- appveyor.yml | 2 -- tox.ini | 1 - 3 files changed, 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 21fb6c7db..938391cde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,10 +31,6 @@ env: matrix: include: - - env: TOXENV=py26 - python: '2.6' - - env: TOXENV=py33 - python: '3.3' - env: TOXENV=pypy python: 'pypy-5.4' - env: TOXENV=py35 diff --git a/appveyor.yml b/appveyor.yml index 01a723d5f..4f4afe15c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,9 +10,7 @@ environment: - TOXENV: "coveralls" # note: please use "tox --listenvs" to populate the build matrix below - TOXENV: "linting" - - TOXENV: "py26" - TOXENV: "py27" - - TOXENV: "py33" - TOXENV: "py34" - TOXENV: "py35" - TOXENV: "py36" diff --git a/tox.ini b/tox.ini index 0f9611e7d..b774cbda5 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ distshare = {homedir}/.tox/distshare envlist = linting py27 - py33 py34 py35 py36 From c48659844060746789f059929ce03a60589cc22b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 9 Oct 2017 16:52:31 +0200 Subject: [PATCH 089/159] remove some support code for old python versions --- _pytest/compat.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/_pytest/compat.py b/_pytest/compat.py index 7bf3bb9b8..99ec54c53 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -4,7 +4,6 @@ python version compatibility code from __future__ import absolute_import, division, print_function import sys import inspect -import types import re import functools import codecs @@ -120,16 +119,6 @@ def getfuncargnames(function, startindex=None, cls=None): return tuple(argnames[startindex:]) -if sys.version_info[:2] == (2, 6): - def isclass(object): - """ Return true if the object is a class. Overrides inspect.isclass for - python 2.6 because it will return True for objects which always return - something on __getattr__ calls (see #1035). - Backport of https://hg.python.org/cpython/rev/35bf8f7a8edc - """ - return isinstance(object, (type, types.ClassType)) - - if _PY3: imap = map izip = zip @@ -230,10 +219,7 @@ def getimfunc(func): try: return func.__func__ except AttributeError: - try: - return func.im_func - except AttributeError: - return func + return func def safe_getattr(object, name, default): From ef732fc51d9d2ace3166cbbad21a608a3d4bfc4d Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 10 Oct 2017 08:54:56 +0300 Subject: [PATCH 090/159] Remove code for unsupported Python versions --- .coveragerc | 4 +--- README.rst | 2 +- _pytest/_argcomplete.py | 3 --- _pytest/_code/source.py | 2 -- _pytest/assertion/__init__.py | 6 ++---- _pytest/assertion/rewrite.py | 5 ----- _pytest/fixtures.py | 5 +---- _pytest/pytester.py | 8 ++------ _pytest/python_api.py | 10 +--------- doc/en/example/multipython.py | 2 +- doc/en/getting-started.rst | 24 +++++++++++------------- doc/en/index.rst | 10 +++++----- doc/en/skipping.rst | 16 ++++++++-------- setup.py | 2 +- testing/acceptance_test.py | 2 +- testing/code/test_excinfo.py | 10 ++-------- testing/code/test_source.py | 2 -- testing/python/approx.py | 6 ------ testing/python/collect.py | 15 ++++----------- testing/test_assertrewrite.py | 2 +- testing/test_config.py | 2 +- testing/test_parseopt.py | 8 +------- testing/test_unittest.py | 5 ----- 23 files changed, 44 insertions(+), 107 deletions(-) diff --git a/.coveragerc b/.coveragerc index 48670b41d..61ff66749 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,4 @@ [run] -omit = +omit = # standlonetemplate is read dynamically and tested by test_genscript *standalonetemplate.py - # oldinterpret could be removed, as it is no longer used in py26+ - *oldinterpret.py diff --git a/README.rst b/README.rst index 15ad6ea18..3630dd4c6 100644 --- a/README.rst +++ b/README.rst @@ -76,7 +76,7 @@ Features - Can run `unittest `_ (or trial), `nose `_ test suites out of the box; -- Python2.6+, Python3.3+, PyPy-2.3, Jython-2.5 (untested); +- Python 2.7, Python 3.4+, PyPy 2.3, Jython 2.5 (untested); - Rich plugin architecture, with over 315+ `external plugins `_ and thriving community; diff --git a/_pytest/_argcomplete.py b/_pytest/_argcomplete.py index 965ec7951..0625a75f9 100644 --- a/_pytest/_argcomplete.py +++ b/_pytest/_argcomplete.py @@ -4,9 +4,6 @@ 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. -argcomplete does not support python 2.5 (although the changes for that -are minor). - Function try_argcomplete(parser) should be called directly before the call to ArgumentParser.parse_args(). diff --git a/_pytest/_code/source.py b/_pytest/_code/source.py index 2959d635a..8b0bc3b44 100644 --- a/_pytest/_code/source.py +++ b/_pytest/_code/source.py @@ -342,8 +342,6 @@ def get_statement_startend2(lineno, node): def getstatementrange_ast(lineno, source, assertion=False, astnode=None): if astnode is None: content = str(source) - if sys.version_info < (2, 7): - content += "\n" try: astnode = compile(content, "source", "exec", 1024) # 1024 for AST except ValueError: diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py index e9e39dae0..a48e98c85 100644 --- a/_pytest/assertion/__init__.py +++ b/_pytest/assertion/__init__.py @@ -67,10 +67,8 @@ class AssertionState: def install_importhook(config): """Try to install the rewrite hook, raise SystemError if it fails.""" - # Both Jython and CPython 2.6.0 have AST bugs that make the - # assertion rewriting hook malfunction. - if (sys.platform.startswith('java') or - sys.version_info[:3] == (2, 6, 0)): + # 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') diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 956ff487f..6800f82e6 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -34,7 +34,6 @@ else: PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT -REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2) ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3 if sys.version_info >= (3, 5): @@ -321,10 +320,6 @@ def _rewrite_test(config, fn): return None, None finally: del state._indecode - # On Python versions which are not 2.7 and less than or equal to 3.1, the - # parser expects *nix newlines. - if REWRITE_NEWLINES: - source = source.replace(RN, N) + N try: tree = ast.parse(source) except SyntaxError: diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index f57031e1a..af993f3f9 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -19,10 +19,7 @@ from _pytest.compat import ( from _pytest.outcomes import fail, TEST_OUTCOME from _pytest.compat import FuncargnamesCompatAttr -if sys.version_info[:2] == (2, 6): - from ordereddict import OrderedDict -else: - from collections import OrderedDict +from collections import OrderedDict def pytest_sessionstart(session): diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 2d35bf80a..345a1acd0 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -112,12 +112,9 @@ class LsofFdLeakChecker(object): # XXX copied from execnet's conftest.py - needs to be merged winpymap = { 'python2.7': r'C:\Python27\python.exe', - 'python2.6': r'C:\Python26\python.exe', - 'python3.1': r'C:\Python31\python.exe', - 'python3.2': r'C:\Python32\python.exe', - 'python3.3': r'C:\Python33\python.exe', 'python3.4': r'C:\Python34\python.exe', 'python3.5': r'C:\Python35\python.exe', + 'python3.6': r'C:\Python36\python.exe', } @@ -143,8 +140,7 @@ def getexecutable(name, cache={}): return executable -@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", - 'pypy', 'pypy3']) +@pytest.fixture(params=['python2.7', 'python3.4', 'pypy', 'pypy3']) def anypython(request): name = request.param executable = getexecutable(name) diff --git a/_pytest/python_api.py b/_pytest/python_api.py index b73e0457c..b52f68810 100644 --- a/_pytest/python_api.py +++ b/_pytest/python_api.py @@ -453,8 +453,7 @@ def raises(expected_exception, *args, **kwargs): This helper produces a ``ExceptionInfo()`` object (see below). - If using Python 2.5 or above, you may use this function as a - context manager:: + You may use this function as a context manager:: >>> with raises(ZeroDivisionError): ... 1/0 @@ -609,13 +608,6 @@ class RaisesContext(object): __tracebackhide__ = True if tp[0] is None: fail(self.message) - if sys.version_info < (2, 7): - # py26: on __exit__() exc_value often does not contain the - # exception value. - # http://bugs.python.org/issue7853 - if not isinstance(tp[1], BaseException): - exc_type, value, traceback = tp - tp = exc_type, exc_type(value), traceback self.excinfo.__init__(tp) suppress_exception = issubclass(self.excinfo.type, self.expected_exception) if sys.version_info[0] == 2 and suppress_exception: diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index 586f44184..66079be7e 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -6,7 +6,7 @@ import py import pytest import _pytest._code -pythonlist = ['python2.6', 'python2.7', 'python3.4', 'python3.5'] +pythonlist = ['python2.7', 'python3.4', 'python3.5'] @pytest.fixture(params=pythonlist) def python1(request, tmpdir): picklefile = tmpdir.join("data.pickle") diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 1571e4f6b..d2eb77d3e 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -1,7 +1,7 @@ Installation and Getting Started =================================== -**Pythons**: Python 2.6,2.7,3.3,3.4,3.5, Jython, PyPy-2.3 +**Pythons**: Python 2.7, 3.4, 3.5, 3.6, Jython, PyPy-2.3 **Platforms**: Unix/Posix and Windows @@ -9,8 +9,6 @@ Installation and Getting Started **dependencies**: `py `_, `colorama (Windows) `_, -`argparse (py26) `_, -`ordereddict (py26) `_. **documentation as PDF**: `download latest `_ @@ -50,17 +48,17 @@ That's it. You can execute the test function now:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - + test_sample.py F - + ======= FAILURES ======== _______ test_answer ________ - + def test_answer(): > assert func(3) == 5 E assert 4 == 5 E + where 4 = func(3) - + test_sample.py:5: AssertionError ======= 1 failed in 0.12 seconds ======== @@ -129,15 +127,15 @@ run the module by passing its filename:: .F ======= FAILURES ======== _______ TestClass.test_two ________ - + self = - + def test_two(self): x = "hello" > assert hasattr(x, 'check') E AssertionError: assert False E + where False = hasattr('hello', 'check') - + test_class.py:8: AssertionError 1 failed, 1 passed in 0.12 seconds @@ -166,14 +164,14 @@ before performing the test function call. Let's just run it:: F ======= FAILURES ======== _______ test_needsfiles ________ - + tmpdir = local('PYTEST_TMPDIR/test_needsfiles0') - + def test_needsfiles(tmpdir): print (tmpdir) > assert 0 E assert 0 - + test_tmpdir.py:3: AssertionError --------------------------- Captured stdout call --------------------------- PYTEST_TMPDIR/test_needsfiles0 diff --git a/doc/en/index.rst b/doc/en/index.rst index 1d2ca57ef..d9414a076 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -28,17 +28,17 @@ To execute it:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - + test_sample.py F - + ======= FAILURES ======== _______ test_answer ________ - + def test_answer(): > assert inc(3) == 5 E assert 4 == 5 E + where 4 = inc(3) - + test_sample.py:5: AssertionError ======= 1 failed in 0.12 seconds ======== @@ -57,7 +57,7 @@ Features - Can run :ref:`unittest ` (including trial) and :ref:`nose ` test suites out of the box; -- Python2.6+, Python3.3+, PyPy-2.3, Jython-2.5 (untested); +- Python 2.7, Python 3.4+, PyPy 2.3, Jython 2.5 (untested); - Rich plugin architecture, with over 315+ `external plugins `_ and thriving community; diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 1504c251c..b87cb1ae5 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -64,11 +64,11 @@ during import time. If you wish to skip something conditionally then you can use ``skipif`` instead. Here is an example of marking a test function to be skipped -when run on a Python3.3 interpreter:: +when run on a Python3.6 interpreter:: import sys - @pytest.mark.skipif(sys.version_info < (3,3), - reason="requires python3.3") + @pytest.mark.skipif(sys.version_info < (3,6), + reason="requires python3.6") def test_function(): ... @@ -250,8 +250,8 @@ You can change the default value of the ``strict`` parameter using the As with skipif_ you can also mark your expectation of a failure on a particular platform:: - @pytest.mark.xfail(sys.version_info >= (3,3), - reason="python3.3 api changes") + @pytest.mark.xfail(sys.version_info >= (3,6), + reason="python3.6 api changes") def test_function(): ... @@ -311,12 +311,12 @@ Running it with the report-on-xfail option gives this output:: platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/example, inifile: collected 7 items - + xfail_demo.py xxxxxxx ======= short test summary info ======== XFAIL xfail_demo.py::test_hello XFAIL xfail_demo.py::test_hello2 - reason: [NOTRUN] + reason: [NOTRUN] XFAIL xfail_demo.py::test_hello3 condition: hasattr(os, 'sep') XFAIL xfail_demo.py::test_hello4 @@ -326,7 +326,7 @@ Running it with the report-on-xfail option gives this output:: XFAIL xfail_demo.py::test_hello6 reason: reason XFAIL xfail_demo.py::test_hello7 - + ======= 7 xfailed in 0.12 seconds ======== .. _`skip/xfail with parametrize`: diff --git a/setup.py b/setup.py index b58a4014c..61e880c2a 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ classifiers = [ 'Topic :: Utilities', ] + [ ('Programming Language :: Python :: %s' % x) - for x in '2.7 3 3.4 3.5 3.6'.split() + for x in '2 2.7 3 3.4 3.5 3.6'.split() ] with open('README.rst') as fd: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 712776906..903e5d499 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -344,7 +344,7 @@ class TestGeneralUsage(object): Importing a module that didn't exist, even if the ImportError was gracefully handled, would make our test crash. - Use recwarn here to silence this warning in Python 2.6 and 2.7: + Use recwarn here to silence this warning in Python 2.7: ImportWarning: Not importing directory '...\not_a_package': missing __init__.py """ testdir.mkdir('not_a_package') diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index f8f8a0365..f7f6123a2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -345,10 +345,7 @@ def test_excinfo_no_sourcecode(): except ValueError: excinfo = _pytest._code.ExceptionInfo() s = str(excinfo.traceback[-1]) - if py.std.sys.version_info < (2, 5): - assert s == " File '':1 in ?\n ???\n" - else: - assert s == " File '':1 in \n ???\n" + assert s == " File '':1 in \n ???\n" def test_excinfo_no_python_sourcecode(tmpdir): @@ -1244,9 +1241,6 @@ def test_no_recursion_index_on_recursion_error(): except: from _pytest._code.code import ExceptionInfo exc_info = ExceptionInfo() - if sys.version_info[:2] == (2, 6): - assert "'RecursionDepthError' object has no attribute '___" in str(exc_info.getrepr()) - else: - assert 'maximum recursion' in str(exc_info.getrepr()) + assert 'maximum recursion' in str(exc_info.getrepr()) else: assert 0 diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 1d315aa9b..ed45f0896 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -273,7 +273,6 @@ class TestSourceParsingAndCompiling(object): assert getstatement(2, source).lines == source.lines[2:3] assert getstatement(3, source).lines == source.lines[3:4] - @pytest.mark.skipif("sys.version_info < (2,6)") def test_getstatementrange_out_of_bounds_py3(self): source = Source("if xxx:\n from .collections import something") r = source.getstatementrange(1) @@ -283,7 +282,6 @@ class TestSourceParsingAndCompiling(object): source = Source(":") pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) - @pytest.mark.skipif("sys.version_info < (2,6)") def test_compile_to_ast(self): import ast source = Source("x = 4") diff --git a/testing/python/approx.py b/testing/python/approx.py index d591b8ba5..300e1ce86 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -24,11 +24,8 @@ class MyDocTestRunner(doctest.DocTestRunner): class TestApprox(object): def test_repr_string(self): - # for some reason in Python 2.6 it is not displaying the tolerance representation correctly plus_minus = u'\u00b1' if sys.version_info[0] > 2 else u'+-' tol1, tol2, infr = '1.0e-06', '2.0e-06', 'inf' - if sys.version_info[:2] == (2, 6): - tol1, tol2, infr = '???', '???', '???' assert repr(approx(1.0)) == '1.0 {pm} {tol1}'.format(pm=plus_minus, tol1=tol1) assert repr(approx([1.0, 2.0])) == 'approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])'.format( pm=plus_minus, tol1=tol1, tol2=tol2) @@ -375,9 +372,6 @@ class TestApprox(object): assert [3] == [pytest.approx(4)] """) expected = '4.0e-06' - # for some reason in Python 2.6 it is not displaying the tolerance representation correctly - if sys.version_info[:2] == (2, 6): - expected = '???' result = testdir.runpytest() result.stdout.fnmatch_lines([ '*At index 0 diff: 3 != 4 * {0}'.format(expected), diff --git a/testing/python/collect.py b/testing/python/collect.py index ccd5c11e7..c3a204568 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -164,17 +164,10 @@ class TestClass(object): assert fix == 1 """) result = testdir.runpytest() - if sys.version_info < (2, 7): - # in 2.6, the code to handle static methods doesn't work - result.stdout.fnmatch_lines([ - "*collected 0 items*", - "*cannot collect static method*", - ]) - else: - result.stdout.fnmatch_lines([ - "*collected 2 items*", - "*2 passed in*", - ]) + result.stdout.fnmatch_lines([ + "*collected 2 items*", + "*2 passed in*", + ]) def test_setup_teardown_class_as_classmethod(self, testdir): testdir.makepyfile(test_mod1=""" diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 2d61b7440..07912f0b7 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -640,7 +640,7 @@ def test_rewritten(): testdir.tmpdir.join("test_newlines.py").write(b, "wb") assert testdir.runpytest().ret == 0 - @pytest.mark.skipif(sys.version_info < (3, 3), + @pytest.mark.skipif(sys.version_info < (3, 4), reason='packages without __init__.py not supported on python 2') def test_package_without__init__py(self, testdir): pkg = testdir.mkdir('a_package_without_init_py') diff --git a/testing/test_config.py b/testing/test_config.py index 3cad6d587..d049725cd 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -139,7 +139,7 @@ class TestConfigAPI(object): assert config.getoption(x) == "this" pytest.raises(ValueError, "config.getoption('qweqwe')") - @pytest.mark.skipif('sys.version_info[:2] not in [(2, 6), (2, 7)]') + @pytest.mark.skipif('sys.version_info[0] < 3') def test_config_getoption_unicode(self, testdir): testdir.makeconftest(""" from __future__ import unicode_literals diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 02fdf0ada..921592570 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -300,13 +300,7 @@ def test_argcomplete(testdir, monkeypatch): elif not result.stdout.str(): pytest.skip("bash provided no output, argcomplete not available?") else: - if py.std.sys.version_info < (2, 7): - result.stdout.lines = result.stdout.lines[0].split('\x0b') - result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"]) - else: - result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"]) - if py.std.sys.version_info < (2, 7): - return + result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"]) os.mkdir('test_argcomplete.d') arg = 'test_argc' monkeypatch.setenv('COMP_LINE', "pytest " + arg) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 8051deda4..2bc1630cd 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -168,7 +168,6 @@ def test_teardown_issue1649(testdir): assert type(obj).__name__ != 'TestCaseObjectsShouldBeCleanedUp' -@pytest.mark.skipif("sys.version_info < (2,7)") def test_unittest_skip_issue148(testdir): testpath = testdir.makepyfile(""" import unittest @@ -629,7 +628,6 @@ def test_unittest_typerror_traceback(testdir): assert result.ret == 1 -@pytest.mark.skipif("sys.version_info < (2,7)") @pytest.mark.parametrize('runner', ['pytest', 'unittest']) def test_unittest_expected_failure_for_failing_test_is_xfail(testdir, runner): script = testdir.makepyfile(""" @@ -656,7 +654,6 @@ def test_unittest_expected_failure_for_failing_test_is_xfail(testdir, runner): assert result.ret == 0 -@pytest.mark.skipif("sys.version_info < (2,7)") @pytest.mark.parametrize('runner', ['pytest', 'unittest']) def test_unittest_expected_failure_for_passing_test_is_fail(testdir, runner): script = testdir.makepyfile(""" @@ -787,7 +784,6 @@ def test_issue333_result_clearing(testdir): reprec.assertoutcome(failed=1) -@pytest.mark.skipif("sys.version_info < (2,7)") def test_unittest_raise_skip_issue748(testdir): testdir.makepyfile(test_foo=""" import unittest @@ -803,7 +799,6 @@ def test_unittest_raise_skip_issue748(testdir): """) -@pytest.mark.skipif("sys.version_info < (2,7)") def test_unittest_skip_issue1169(testdir): testdir.makepyfile(test_foo=""" import unittest From 7113c76f0d76378e16b54b5d8d4dc466e1f38935 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 10 Oct 2017 09:03:26 +0300 Subject: [PATCH 091/159] Remove unused import --- testing/code/test_excinfo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index f7f6123a2..2086748e9 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function -import sys import operator import _pytest import py From be0e2132b73c3dced78e0a453a5f86f5c57630db Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 10 Oct 2017 10:01:15 +0300 Subject: [PATCH 092/159] Update authors [CI skip] --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index cf31e0389..f7cad051b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -71,6 +71,7 @@ Grig Gheorghiu Grigorii Eremeev (budulianin) Guido Wesdorp Harald Armin Massa +Hugo van Kemenade Hui Wang (coldnight) Ian Bicking Jaap Broekhuizen From 66e9a794726fd4ba6dc7e28962cdb46b46297e9b Mon Sep 17 00:00:00 2001 From: Dirk Thomas Date: Tue, 10 Oct 2017 07:56:46 -0700 Subject: [PATCH 093/159] get PYTEST_ADDOPTS before calling _initini --- _pytest/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_pytest/config.py b/_pytest/config.py index 2f28bb7bf..1c0cd185d 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -1055,9 +1055,10 @@ class Config(object): "(are you using python -O?)\n") def _preparse(self, args, addopts=True): - self._initini(args) if addopts: args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args + self._initini(args) + if addopts: args[:] = self.getini("addopts") + args self._checkversion() self._consider_importhook(args) From ed7aa074aa6b17befed57264a2414e633f792109 Mon Sep 17 00:00:00 2001 From: Dirk Thomas Date: Tue, 10 Oct 2017 08:13:07 -0700 Subject: [PATCH 094/159] add changelog file for #2824 --- changelog/2824.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2824.feature diff --git a/changelog/2824.feature b/changelog/2824.feature new file mode 100644 index 000000000..690ca939a --- /dev/null +++ b/changelog/2824.feature @@ -0,0 +1 @@ +Allow setting ``file_or_dir``, ``-c``, and ``-o`` in PYTEST_ADDOPTS. From ce8c829945ef30352cc7bec750b91eda85eba8f9 Mon Sep 17 00:00:00 2001 From: Dirk Thomas Date: Tue, 10 Oct 2017 10:03:15 -0700 Subject: [PATCH 095/159] add test for #2824 --- testing/test_config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testing/test_config.py b/testing/test_config.py index d049725cd..93b288215 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -843,3 +843,11 @@ class TestOverrideIniArgs(object): rootdir, inifile, inicfg = determine_setup(None, ['a/exist']) assert rootdir == tmpdir assert inifile is None + + def test_addopts_before_initini(self, testdir, tmpdir, monkeypatch): + cache_dir = testdir.tmpdir.join('.custom_cache') + monkeypatch.setenv('PYTEST_ADDOPTS', '-o cache_dir=%s' % cache_dir) + from _pytest.config import get_config + config = get_config() + config._preparse([], addopts=True) + assert config._override_ini == [['cache_dir=%s' % cache_dir]] From 10a3b9118b70ae287b32c218d89a5a5e0c145127 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 11 Oct 2017 19:37:17 -0300 Subject: [PATCH 096/159] Use a relative cache_dir in test because of how arguments are parsed on Windows We use shlex to parse command-line arguments and PYTEST_ADDOPTS, so passing a full path with '\' arguments produces incorrect results on Windows Anyway users are advised to use relative paths for portability --- testing/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_config.py b/testing/test_config.py index 93b288215..9881a5d41 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -845,7 +845,7 @@ class TestOverrideIniArgs(object): assert inifile is None def test_addopts_before_initini(self, testdir, tmpdir, monkeypatch): - cache_dir = testdir.tmpdir.join('.custom_cache') + cache_dir = '.custom_cache' monkeypatch.setenv('PYTEST_ADDOPTS', '-o cache_dir=%s' % cache_dir) from _pytest.config import get_config config = get_config() From 69f3bd83360d7fb13df7e3e46eb16f029b86517d Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 22 Sep 2017 22:34:32 +0200 Subject: [PATCH 097/159] Add changelog entry for catchlog plugin --- changelog/2794.feature | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/2794.feature diff --git a/changelog/2794.feature b/changelog/2794.feature new file mode 100644 index 000000000..9cff1c4bb --- /dev/null +++ b/changelog/2794.feature @@ -0,0 +1,3 @@ +Pytest now captures and displays output from the standard `logging` module. The user can control the logging level to be captured by specifying options in ``pytest.ini``, the command line and also during individual tests using markers. Also, a ``caplog`` fixture is available that enables users to test the captured log during specific tests (similar to ``capsys`` for example). For more information, please see the `docs `_. + +This feature was introduced by merging the popular `pytest-catchlog `_ plugin, thanks to `Thomas Hisch `_. Be advised that during the merging the backward compatibility interface with the defunct ``pytest-capturelog`` has been dropped. \ No newline at end of file From 0ec72d07450635cef2c0082fc3f82f1e842a92d5 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 26 Sep 2017 22:58:26 +0200 Subject: [PATCH 098/159] Improve get_option_ini and get_actual_log_level --- _pytest/logging.py | 71 ++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index b8533da2f..950b0f512 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import logging from contextlib import closing, contextmanager import sys +import six import pytest import py @@ -12,12 +13,13 @@ DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s' DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S' -def get_option_ini(config, name): - ret = config.getoption(name) # 'default' arg won't work as expected - if ret is None: - ret = config.getini(name) - return ret - +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.""" @@ -221,12 +223,19 @@ def caplog(request): return LogCaptureFixture(request.node) -def get_actual_log_level(config, setting_name): +def get_actual_log_level(config, *setting_names): """Return the actual logging level.""" - log_level = get_option_ini(config, setting_name) - if not log_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, py.builtin.text): + + if isinstance(log_level, six.string_types): log_level = log_level.upper() try: return int(getattr(logging, log_level, log_level)) @@ -254,15 +263,8 @@ class LoggingPlugin(object): The formatter can be safely shared across all handlers so create a single one for the entire test session here. """ - log_cli_level = get_actual_log_level(config, 'log_cli_level') - if log_cli_level is None: - # No specific CLI logging level was provided, let's check - # log_level for a fallback - log_cli_level = get_actual_log_level(config, 'log_level') - if log_cli_level is None: - # No log_level was provided, default to WARNING - log_cli_level = logging.WARNING - self.log_cli_level = log_cli_level + self.log_cli_level = get_actual_log_level( + config, 'log_cli_level', 'log_level') or logging.WARNING self.print_logs = get_option_ini(config, 'log_print') self.formatter = logging.Formatter( @@ -270,14 +272,10 @@ class LoggingPlugin(object): get_option_ini(config, 'log_date_format')) log_cli_handler = logging.StreamHandler(sys.stderr) - log_cli_format = get_option_ini(config, 'log_cli_format') - if not log_cli_format: - # No CLI specific format was provided, use log_format - log_cli_format = get_option_ini(config, 'log_format') - log_cli_date_format = get_option_ini(config, 'log_cli_date_format') - if not log_cli_date_format: - # No CLI specific date format was provided, use log_date_format - log_cli_date_format = get_option_ini(config, 'log_date_format') + log_cli_format = get_option_ini( + config, 'log_cli_format', 'log_format') + log_cli_date_format = get_option_ini( + config, 'log_cli_date_format', 'log_date_format') log_cli_formatter = logging.Formatter( log_cli_format, datefmt=log_cli_date_format) @@ -288,20 +286,13 @@ class LoggingPlugin(object): log_file = get_option_ini(config, 'log_file') if log_file: - log_file_level = get_actual_log_level(config, 'log_file_level') - if log_file_level is None: - # No log_level was provided, default to WARNING - log_file_level = logging.WARNING - self.log_file_level = log_file_level + self.log_file_level = get_actual_log_level( + config, 'log_file_level') or logging.WARNING - log_file_format = get_option_ini(config, 'log_file_format') - if not log_file_format: - # No log file specific format was provided, use log_format - log_file_format = get_option_ini(config, 'log_format') - log_file_date_format = get_option_ini(config, 'log_file_date_format') - if not log_file_date_format: - # No log file specific date format was provided, use log_date_format - log_file_date_format = get_option_ini(config, 'log_date_format') + log_file_format = get_option_ini( + config, 'log_file_format', 'log_format') + log_file_date_format = get_option_ini( + config, 'log_file_date_format', 'log_date_format') self.log_file_handler = logging.FileHandler( log_file, # Each pytest runtests session will write to a clean logfile From 1f3ab118fae69ddd60677289173b9726f7b5c068 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 26 Sep 2017 23:05:42 +0200 Subject: [PATCH 099/159] Remove usage of get_logger_obj --- _pytest/logging.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 950b0f512..76d5be34b 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -78,20 +78,10 @@ def pytest_addoption(parser): help='log date format as used by the logging module.') -def get_logger_obj(logger=None): - """Get a logger object that can be specified by its name, or passed as is. - - Defaults to the root logger. - """ - if logger is None or isinstance(logger, py.builtin._basestring): - logger = logging.getLogger(logger) - return logger - - @contextmanager def logging_using_handler(handler, logger=None): """Context manager that safely registers a given handler.""" - logger = get_logger_obj(logger) + logger = logger or logging.getLogger(logger) if handler in logger.handlers: # reentrancy # Adding the same handler twice would confuse logging system. @@ -109,7 +99,7 @@ def logging_using_handler(handler, logger=None): def catching_logs(handler, formatter=None, level=logging.NOTSET, logger=None): """Context manager that prepares the whole logging machinery properly.""" - logger = get_logger_obj(logger) + logger = logger or logging.getLogger(logger) if formatter is not None: handler.setFormatter(formatter) From 815dd19fb4b265e9a29ed98bf612953858fb3bbd Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 26 Sep 2017 23:27:38 +0200 Subject: [PATCH 100/159] Remove unicode literal compat code --- testing/logging/test_fixture.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 4072234d8..b5bee4233 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -1,13 +1,10 @@ # -*- coding: utf-8 -*- -import sys import logging logger = logging.getLogger(__name__) sublogger = logging.getLogger(__name__+'.baz') -u = (lambda x: x.decode('utf-8')) if sys.version_info < (3,) else (lambda x: x) - def test_fixture_help(testdir): result = testdir.runpytest('--fixtures') @@ -60,14 +57,14 @@ def test_record_tuples(caplog): def test_unicode(caplog): - logger.info(u('bū')) + logger.info(u'bū') assert caplog.records[0].levelname == 'INFO' - assert caplog.records[0].msg == u('bū') - assert u('bū') in caplog.text + assert caplog.records[0].msg == u'bū' + assert u'bū' in caplog.text def test_clear(caplog): - logger.info(u('bū')) + logger.info(u'bū') assert len(caplog.records) caplog.clear() assert not len(caplog.records) From 0e83511d6d6fa0f6619df8a17b7c04de8e21acba Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 26 Sep 2017 23:34:54 +0200 Subject: [PATCH 101/159] Rename name of registered logging plugin --- _pytest/logging.py | 3 ++- testing/logging/test_reporting.py | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 76d5be34b..1b71a544e 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -240,7 +240,8 @@ def get_actual_log_level(config, *setting_names): def pytest_configure(config): - config.pluginmanager.register(LoggingPlugin(config), 'loggingp') + config.pluginmanager.register(LoggingPlugin(config), + 'logging-plugin') class LoggingPlugin(object): diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index af75eb632..c02ee2172 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -147,7 +147,7 @@ def test_log_cli_default_level(testdir): import pytest import logging def test_log_cli(request): - plugin = request.config.pluginmanager.getplugin('loggingp') + plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_cli_handler.level == logging.WARNING logging.getLogger('catchlog').info("This log message won't be shown") logging.getLogger('catchlog').warning("This log message will be shown") @@ -180,7 +180,7 @@ def test_log_cli_level(testdir): import pytest import logging def test_log_cli(request): - plugin = request.config.pluginmanager.getplugin('loggingp') + plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_cli_handler.level == logging.INFO logging.getLogger('catchlog').debug("This log message won't be shown") logging.getLogger('catchlog').info("This log message will be shown") @@ -236,7 +236,7 @@ def test_log_cli_ini_level(testdir): import pytest import logging def test_log_cli(request): - plugin = request.config.pluginmanager.getplugin('loggingp') + plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_cli_handler.level == logging.INFO logging.getLogger('catchlog').debug("This log message won't be shown") logging.getLogger('catchlog').info("This log message will be shown") @@ -269,7 +269,7 @@ def test_log_file_cli(testdir): import pytest import logging def test_log_file(request): - plugin = request.config.pluginmanager.getplugin('loggingp') + plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_file_handler.level == logging.WARNING logging.getLogger('catchlog').info("This log message won't be shown") logging.getLogger('catchlog').warning("This log message will be shown") @@ -300,7 +300,7 @@ def test_log_file_cli_level(testdir): import pytest import logging def test_log_file(request): - plugin = request.config.pluginmanager.getplugin('loggingp') + plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_file_handler.level == logging.INFO logging.getLogger('catchlog').debug("This log message won't be shown") logging.getLogger('catchlog').info("This log message will be shown") @@ -339,7 +339,7 @@ def test_log_file_ini(testdir): import pytest import logging def test_log_file(request): - plugin = request.config.pluginmanager.getplugin('loggingp') + plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_file_handler.level == logging.WARNING logging.getLogger('catchlog').info("This log message won't be shown") logging.getLogger('catchlog').warning("This log message will be shown") @@ -375,7 +375,7 @@ def test_log_file_ini_level(testdir): import pytest import logging def test_log_file(request): - plugin = request.config.pluginmanager.getplugin('loggingp') + plugin = request.config.pluginmanager.getplugin('logging-plugin') assert plugin.log_file_handler.level == logging.INFO logging.getLogger('catchlog').debug("This log message won't be shown") logging.getLogger('catchlog').info("This log message will be shown") From 502652ff02fb6566cd88df865fe6ec857742e172 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Tue, 26 Sep 2017 23:54:28 +0200 Subject: [PATCH 102/159] Add preliminary documentation for logging-plugin --- doc/en/usage.rst | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/doc/en/usage.rst b/doc/en/usage.rst index a8c6d40a0..842604d4b 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -189,6 +189,187 @@ in your code and pytest automatically disables its output capture for that test: for test output occurring after you exit the interactive PDB_ tracing session and continue with the regular test run. +.. _logging: + +Logging +------- + +.. versionadded 3.3.0 + +Log messages are captured by default and for each failed test will be shown in +the same manner as captured stdout and stderr. + +Running without options:: + + pytest + +Shows failed tests like so:: + + ----------------------- Captured stdlog call ---------------------- + test_reporting.py 26 INFO text going to logger + ----------------------- Captured stdout call ---------------------- + text going to stdout + ----------------------- Captured stderr call ---------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +By default each captured log message shows the module, line number, log level +and message. Showing the exact module and line number is useful for testing and +debugging. If desired the log format and date format can be specified to +anything that the logging module supports. + +Running pytest specifying formatting options:: + + pytest --log-format="%(asctime)s %(levelname)s %(message)s" \ + --log-date-format="%Y-%m-%d %H:%M:%S" + +Shows failed tests like so:: + + ----------------------- Captured stdlog call ---------------------- + 2010-04-10 14:48:44 INFO text going to logger + ----------------------- Captured stdout call ---------------------- + text going to stdout + ----------------------- Captured stderr call ---------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +These options can also be customized through a configuration file:: + + [pytest] + log_format = %(asctime)s %(levelname)s %(message)s + log_date_format = %Y-%m-%d %H:%M:%S + +Although the same effect could be achieved through the ``addopts`` setting, +using dedicated options should be preferred since the latter doesn't force other +developers to have ``pytest-catchlog`` installed (while at the same time, +``addopts`` approach would fail with 'unrecognized arguments' error). Command +line arguments take precedence. + +Further it is possible to disable reporting logs on failed tests completely +with:: + + pytest --no-print-logs + +Or in you ``pytest.ini``:: + + [pytest] + log_print=False + + +Shows failed tests in the normal manner as no logs were captured:: + + ----------------------- Captured stdout call ---------------------- + text going to stdout + ----------------------- Captured stderr call ---------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +Inside tests it is possible to change the log level for the captured log +messages. This is supported by the ``caplog`` fixture:: + + def test_foo(caplog): + caplog.set_level(logging.INFO) + pass + +By default the level is set on the handler used to catch the log messages, +however as a convenience it is also possible to set the log level of any +logger:: + + def test_foo(caplog): + caplog.set_level(logging.CRITICAL, logger='root.baz') + pass + +It is also possible to use a context manager to temporarily change the log +level:: + + def test_bar(caplog): + with caplog.at_level(logging.INFO): + pass + +Again, by default the level of the handler is affected but the level of any +logger can be changed instead with:: + + def test_bar(caplog): + with caplog.at_level(logging.CRITICAL, logger='root.baz'): + pass + +Lastly all the logs sent to the logger during the test run are made available on +the fixture in the form of both the LogRecord instances and the final log text. +This is useful for when you want to assert on the contents of a message:: + + def test_baz(caplog): + func_under_test() + for record in caplog.records: + assert record.levelname != 'CRITICAL' + assert 'wally' not in caplog.text + +For all the available attributes of the log records see the +``logging.LogRecord`` class. + +You can also resort to ``record_tuples`` if all you want to do is to ensure, +that certain messages have been logged under a given logger name with a given +severity and message:: + + def test_foo(caplog): + logging.getLogger().info('boo %s', 'arg') + + assert caplog.record_tuples == [ + ('root', logging.INFO, 'boo arg'), + ] + +You can call ``caplog.clear()`` to reset the captured log records in a test:: + + def test_something_with_clearing_records(caplog): + some_method_that_creates_log_records() + caplog.clear() + your_test_method() + assert ['Foo'] == [rec.message for rec in caplog.records] + +Live Logs +^^^^^^^^^ + +By default, catchlog will output any logging records with a level higher or +equal to WARNING. In order to actually see these logs in the console you have to +disable pytest output capture by passing ``-s``. + +You can specify the logging level for which log records with equal or higher +level are printed to the console by passing ``--log-cli-level``. This setting +accepts the logging level names as seen in python's documentation or an integer +as the logging level num. + +Additionally, you can also specify ``--log-cli-format`` and +``--log-cli-date-format`` which mirror and default to ``--log-format`` and +``--log-date-format`` if not provided, but are applied only to the console +logging handler. + +All of the CLI log options can also be set in the configuration INI file. The +option names are: + +* ``log_cli_level`` +* ``log_cli_format`` +* ``log_cli_date_format`` + +If you need to record the whole test suite logging calls to a file, you can pass +``--log-file=/path/to/log/file``. This log file is opened in write mode which +means that it will be overwritten at each run tests session. + +You can also specify the logging level for the log file by passing +``--log-file-level``. This setting accepts the logging level names as seen in +python's documentation(ie, uppercased level names) or an integer as the logging +level num. + +Additionally, you can also specify ``--log-file-format`` and +``--log-file-date-format`` which are equal to ``--log-format`` and +``--log-date-format`` but are applied to the log file logging handler. + +All of the log file options can also be set in the configuration INI file. The +option names are: + +* ``log_file`` +* ``log_file_level`` +* ``log_file_format`` +* ``log_file_date_format`` + .. _durations: From 775f4a6f2fe031e0ab01e8579cefbd528d68770d Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 27 Sep 2017 20:22:44 +0200 Subject: [PATCH 103/159] Fix flake8 issue --- _pytest/logging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/_pytest/logging.py b/_pytest/logging.py index 1b71a544e..2d33f98ea 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -21,6 +21,7 @@ def get_option_ini(config, *names): if ret: return ret + def pytest_addoption(parser): """Add options to control log capturing.""" From f3261d94184280a2bc9ce9a1336c9f08cbd7a3c6 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 27 Sep 2017 00:39:41 +0200 Subject: [PATCH 104/159] Move logging docu into own rst file Remove reference of pytest-catchlog plugin in plugins.rst --- doc/en/contents.rst | 1 + doc/en/logging.rst | 192 ++++++++++++++++++++++++++++++++++++++++++++ doc/en/plugins.rst | 3 - doc/en/usage.rst | 182 ----------------------------------------- 4 files changed, 193 insertions(+), 185 deletions(-) create mode 100644 doc/en/logging.rst diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 028414eb6..12dbce2ee 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -30,6 +30,7 @@ Full pytest documentation xunit_setup plugins writing_plugins + logging goodpractices pythonpath diff --git a/doc/en/logging.rst b/doc/en/logging.rst new file mode 100644 index 000000000..e3bf56038 --- /dev/null +++ b/doc/en/logging.rst @@ -0,0 +1,192 @@ +.. _logging: + +Logging +------- + +.. versionadded 3.3.0 + +.. note:: + + This feature is a drop-in replacement for the `pytest-catchlog + `_ plugin and they will conflict + with each other. The backward compatibility API with ``pytest-capturelog`` + has been dropped when this feature was introduced, so if for that reason you + still need ``pytest-catchlog`` you can disable the internal feature by + adding to your ``pytest.ini``: + + .. code-block:: ini + + [pytest] + addopts=-p no:logging + +Log messages are captured by default and for each failed test will be shown in +the same manner as captured stdout and stderr. + +Running without options:: + + pytest + +Shows failed tests like so:: + + ----------------------- Captured stdlog call ---------------------- + test_reporting.py 26 INFO text going to logger + ----------------------- Captured stdout call ---------------------- + text going to stdout + ----------------------- Captured stderr call ---------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +By default each captured log message shows the module, line number, log level +and message. Showing the exact module and line number is useful for testing and +debugging. If desired the log format and date format can be specified to +anything that the logging module supports. + +Running pytest specifying formatting options:: + + pytest --log-format="%(asctime)s %(levelname)s %(message)s" \ + --log-date-format="%Y-%m-%d %H:%M:%S" + +Shows failed tests like so:: + + ----------------------- Captured stdlog call ---------------------- + 2010-04-10 14:48:44 INFO text going to logger + ----------------------- Captured stdout call ---------------------- + text going to stdout + ----------------------- Captured stderr call ---------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +These options can also be customized through a configuration file: + +.. code-block:: ini + + [pytest] + log_format = %(asctime)s %(levelname)s %(message)s + log_date_format = %Y-%m-%d %H:%M:%S + +Further it is possible to disable reporting logs on failed tests completely +with:: + + pytest --no-print-logs + +Or in you ``pytest.ini``: + +.. code-block:: ini + + [pytest] + log_print = False + + +Shows failed tests in the normal manner as no logs were captured:: + + ----------------------- Captured stdout call ---------------------- + text going to stdout + ----------------------- Captured stderr call ---------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +Inside tests it is possible to change the log level for the captured log +messages. This is supported by the ``caplog`` fixture:: + + def test_foo(caplog): + caplog.set_level(logging.INFO) + pass + +By default the level is set on the handler used to catch the log messages, +however as a convenience it is also possible to set the log level of any +logger:: + + def test_foo(caplog): + caplog.set_level(logging.CRITICAL, logger='root.baz') + pass + +It is also possible to use a context manager to temporarily change the log +level:: + + def test_bar(caplog): + with caplog.at_level(logging.INFO): + pass + +Again, by default the level of the handler is affected but the level of any +logger can be changed instead with:: + + def test_bar(caplog): + with caplog.at_level(logging.CRITICAL, logger='root.baz'): + pass + +Lastly all the logs sent to the logger during the test run are made available on +the fixture in the form of both the LogRecord instances and the final log text. +This is useful for when you want to assert on the contents of a message:: + + def test_baz(caplog): + func_under_test() + for record in caplog.records: + assert record.levelname != 'CRITICAL' + assert 'wally' not in caplog.text + +For all the available attributes of the log records see the +``logging.LogRecord`` class. + +You can also resort to ``record_tuples`` if all you want to do is to ensure, +that certain messages have been logged under a given logger name with a given +severity and message:: + + def test_foo(caplog): + logging.getLogger().info('boo %s', 'arg') + + assert caplog.record_tuples == [ + ('root', logging.INFO, 'boo arg'), + ] + +You can call ``caplog.clear()`` to reset the captured log records in a test:: + + def test_something_with_clearing_records(caplog): + some_method_that_creates_log_records() + caplog.clear() + your_test_method() + assert ['Foo'] == [rec.message for rec in caplog.records] + +Live Logs +^^^^^^^^^ + +By default, pytest will output any logging records with a level higher or +equal to WARNING. In order to actually see these logs in the console you have to +disable pytest output capture by passing ``-s``. + +You can specify the logging level for which log records with equal or higher +level are printed to the console by passing ``--log-cli-level``. This setting +accepts the logging level names as seen in python's documentation or an integer +as the logging level num. + +Additionally, you can also specify ``--log-cli-format`` and +``--log-cli-date-format`` which mirror and default to ``--log-format`` and +``--log-date-format`` if not provided, but are applied only to the console +logging handler. + +All of the CLI log options can also be set in the configuration INI file. The +option names are: + +* ``log_cli_level`` +* ``log_cli_format`` +* ``log_cli_date_format`` + +If you need to record the whole test suite logging calls to a file, you can pass +``--log-file=/path/to/log/file``. This log file is opened in write mode which +means that it will be overwritten at each run tests session. + +You can also specify the logging level for the log file by passing +``--log-file-level``. This setting accepts the logging level names as seen in +python's documentation(ie, uppercased level names) or an integer as the logging +level num. + +Additionally, you can also specify ``--log-file-format`` and +``--log-file-date-format`` which are equal to ``--log-format`` and +``--log-date-format`` but are applied to the log file logging handler. + +All of the log file options can also be set in the configuration INI file. The +option names are: + +* ``log_file`` +* ``log_file_level`` +* ``log_file_format`` +* ``log_file_date_format`` diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index ec031e9e0..bba7d3ecd 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -27,9 +27,6 @@ Here is a little annotated list for some popular plugins: for `twisted `_ apps, starting a reactor and processing deferreds from test functions. -* `pytest-catchlog `_: - to capture and assert about messages from the logging module - * `pytest-cov `_: coverage reporting, compatible with distributed testing diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 842604d4b..1cb64ec87 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -189,188 +189,6 @@ in your code and pytest automatically disables its output capture for that test: for test output occurring after you exit the interactive PDB_ tracing session and continue with the regular test run. -.. _logging: - -Logging -------- - -.. versionadded 3.3.0 - -Log messages are captured by default and for each failed test will be shown in -the same manner as captured stdout and stderr. - -Running without options:: - - pytest - -Shows failed tests like so:: - - ----------------------- Captured stdlog call ---------------------- - test_reporting.py 26 INFO text going to logger - ----------------------- Captured stdout call ---------------------- - text going to stdout - ----------------------- Captured stderr call ---------------------- - text going to stderr - ==================== 2 failed in 0.02 seconds ===================== - -By default each captured log message shows the module, line number, log level -and message. Showing the exact module and line number is useful for testing and -debugging. If desired the log format and date format can be specified to -anything that the logging module supports. - -Running pytest specifying formatting options:: - - pytest --log-format="%(asctime)s %(levelname)s %(message)s" \ - --log-date-format="%Y-%m-%d %H:%M:%S" - -Shows failed tests like so:: - - ----------------------- Captured stdlog call ---------------------- - 2010-04-10 14:48:44 INFO text going to logger - ----------------------- Captured stdout call ---------------------- - text going to stdout - ----------------------- Captured stderr call ---------------------- - text going to stderr - ==================== 2 failed in 0.02 seconds ===================== - -These options can also be customized through a configuration file:: - - [pytest] - log_format = %(asctime)s %(levelname)s %(message)s - log_date_format = %Y-%m-%d %H:%M:%S - -Although the same effect could be achieved through the ``addopts`` setting, -using dedicated options should be preferred since the latter doesn't force other -developers to have ``pytest-catchlog`` installed (while at the same time, -``addopts`` approach would fail with 'unrecognized arguments' error). Command -line arguments take precedence. - -Further it is possible to disable reporting logs on failed tests completely -with:: - - pytest --no-print-logs - -Or in you ``pytest.ini``:: - - [pytest] - log_print=False - - -Shows failed tests in the normal manner as no logs were captured:: - - ----------------------- Captured stdout call ---------------------- - text going to stdout - ----------------------- Captured stderr call ---------------------- - text going to stderr - ==================== 2 failed in 0.02 seconds ===================== - -Inside tests it is possible to change the log level for the captured log -messages. This is supported by the ``caplog`` fixture:: - - def test_foo(caplog): - caplog.set_level(logging.INFO) - pass - -By default the level is set on the handler used to catch the log messages, -however as a convenience it is also possible to set the log level of any -logger:: - - def test_foo(caplog): - caplog.set_level(logging.CRITICAL, logger='root.baz') - pass - -It is also possible to use a context manager to temporarily change the log -level:: - - def test_bar(caplog): - with caplog.at_level(logging.INFO): - pass - -Again, by default the level of the handler is affected but the level of any -logger can be changed instead with:: - - def test_bar(caplog): - with caplog.at_level(logging.CRITICAL, logger='root.baz'): - pass - -Lastly all the logs sent to the logger during the test run are made available on -the fixture in the form of both the LogRecord instances and the final log text. -This is useful for when you want to assert on the contents of a message:: - - def test_baz(caplog): - func_under_test() - for record in caplog.records: - assert record.levelname != 'CRITICAL' - assert 'wally' not in caplog.text - -For all the available attributes of the log records see the -``logging.LogRecord`` class. - -You can also resort to ``record_tuples`` if all you want to do is to ensure, -that certain messages have been logged under a given logger name with a given -severity and message:: - - def test_foo(caplog): - logging.getLogger().info('boo %s', 'arg') - - assert caplog.record_tuples == [ - ('root', logging.INFO, 'boo arg'), - ] - -You can call ``caplog.clear()`` to reset the captured log records in a test:: - - def test_something_with_clearing_records(caplog): - some_method_that_creates_log_records() - caplog.clear() - your_test_method() - assert ['Foo'] == [rec.message for rec in caplog.records] - -Live Logs -^^^^^^^^^ - -By default, catchlog will output any logging records with a level higher or -equal to WARNING. In order to actually see these logs in the console you have to -disable pytest output capture by passing ``-s``. - -You can specify the logging level for which log records with equal or higher -level are printed to the console by passing ``--log-cli-level``. This setting -accepts the logging level names as seen in python's documentation or an integer -as the logging level num. - -Additionally, you can also specify ``--log-cli-format`` and -``--log-cli-date-format`` which mirror and default to ``--log-format`` and -``--log-date-format`` if not provided, but are applied only to the console -logging handler. - -All of the CLI log options can also be set in the configuration INI file. The -option names are: - -* ``log_cli_level`` -* ``log_cli_format`` -* ``log_cli_date_format`` - -If you need to record the whole test suite logging calls to a file, you can pass -``--log-file=/path/to/log/file``. This log file is opened in write mode which -means that it will be overwritten at each run tests session. - -You can also specify the logging level for the log file by passing -``--log-file-level``. This setting accepts the logging level names as seen in -python's documentation(ie, uppercased level names) or an integer as the logging -level num. - -Additionally, you can also specify ``--log-file-format`` and -``--log-file-date-format`` which are equal to ``--log-format`` and -``--log-date-format`` but are applied to the log file logging handler. - -All of the log file options can also be set in the configuration INI file. The -option names are: - -* ``log_file`` -* ``log_file_level`` -* ``log_file_format`` -* ``log_file_date_format`` - - .. _durations: Profiling test execution duration From 8aed5fecd9f5b3b715b4cb00c6e2b72dd86f6e70 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Thu, 12 Oct 2017 01:34:03 +0200 Subject: [PATCH 105/159] Remove test_logging_initialized_in_test --- testing/test_capture.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 79e34a40f..336ca9ef0 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -342,23 +342,6 @@ class TestLoggingInteraction(object): # verify proper termination assert "closed" not in s - def test_logging_initialized_in_test(self, testdir): - p = testdir.makepyfile(""" - def test_something(): - import logging - logging.basicConfig() - logging.warn("hello432") - assert 0 - """) - result = testdir.runpytest_subprocess( - p, "--traceconfig", - "-p", "no:capturelog", "-p", "no:hypothesis", "-p", "no:hypothesispytest") - assert result.ret != 0 - result.stdout.fnmatch_lines([ - "*hello432*", - ]) - assert 'operation on closed file' not in result.stderr.str() - def test_conftestlogging_is_shown(self, testdir): testdir.makeconftest(""" import logging From af75ca435b91d7f487c379831be8821630cf5458 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Thu, 12 Oct 2017 01:48:39 +0200 Subject: [PATCH 106/159] Fix some coding-style issues in the logging plugin --- _pytest/logging.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/_pytest/logging.py b/_pytest/logging.py index 2d33f98ea..ed4db25ad 100644 --- a/_pytest/logging.py +++ b/_pytest/logging.py @@ -24,7 +24,6 @@ def get_option_ini(config, *names): 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): @@ -120,13 +119,11 @@ class LogCaptureHandler(logging.StreamHandler): 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) @@ -260,8 +257,8 @@ class LoggingPlugin(object): self.print_logs = get_option_ini(config, 'log_print') self.formatter = logging.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')) log_cli_handler = logging.StreamHandler(sys.stderr) log_cli_format = get_option_ini( @@ -269,8 +266,8 @@ class LoggingPlugin(object): log_cli_date_format = get_option_ini( config, 'log_cli_date_format', 'log_date_format') log_cli_formatter = logging.Formatter( - log_cli_format, - datefmt=log_cli_date_format) + log_cli_format, + datefmt=log_cli_date_format) self.log_cli_handler = log_cli_handler # needed for a single unittest self.live_logs = catching_logs(log_cli_handler, formatter=log_cli_formatter, @@ -288,11 +285,10 @@ class LoggingPlugin(object): self.log_file_handler = logging.FileHandler( log_file, # Each pytest runtests session will write to a clean logfile - mode='w', - ) + mode='w') log_file_formatter = logging.Formatter( - log_file_format, - datefmt=log_file_date_format) + log_file_format, + datefmt=log_file_date_format) self.log_file_handler.setFormatter(log_file_formatter) else: self.log_file_handler = None From b29a9711c4cb236f9515fca8f36d7733a8301738 Mon Sep 17 00:00:00 2001 From: je Date: Fri, 13 Oct 2017 18:57:52 +0800 Subject: [PATCH 107/159] ignore valid setup.py during --doctest-modules --- _pytest/doctest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/_pytest/doctest.py b/_pytest/doctest.py index cc505c8d0..c1b6a81a8 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -50,12 +50,20 @@ def pytest_addoption(parser): def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": - if config.option.doctestmodules: + 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 + with open(path.strpath, 'r') as f: + contents = f.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 From eaf38c72391efcfde8efc852c0921eb6374787de Mon Sep 17 00:00:00 2001 From: je Date: Sat, 14 Oct 2017 00:47:02 +0800 Subject: [PATCH 108/159] call path.read(), add tests, add news fragment --- AUTHORS | 1 + _pytest/doctest.py | 3 +-- changelog/502.feature | 1 + testing/test_doctest.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 changelog/502.feature diff --git a/AUTHORS b/AUTHORS index f1769116d..026c40fb7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -82,6 +82,7 @@ Jason R. Coombs Javier Domingo Cansino Javier Romero Jeff Widman +John Eddie Ayson John Towler Jon Sonesen Jonas Obrist diff --git a/_pytest/doctest.py b/_pytest/doctest.py index c1b6a81a8..6016265a5 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -59,8 +59,7 @@ def pytest_collect_file(path, parent): def _is_setup_py(config, path, parent): if path.basename != "setup.py": return False - with open(path.strpath, 'r') as f: - contents = f.read() + contents = path.read() return 'setuptools' in contents or 'distutils' in contents diff --git a/changelog/502.feature b/changelog/502.feature new file mode 100644 index 000000000..f768b650e --- /dev/null +++ b/changelog/502.feature @@ -0,0 +1 @@ +Implement feature to skip valid setup.py files when ran with --doctest-modules diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 8a81ea0ed..b8fa1fb77 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -561,6 +561,34 @@ class TestDoctests(object): reportinfo = items[0].reportinfo() assert reportinfo[1] == 1 + def test_valid_setup_py(self, testdir): + ''' + Test to make sure that pytest ignores valid setup.py files when ran + with --doctest-modules + ''' + p = testdir.makepyfile(setup=""" + from setuptools import setup, find_packages + setup(name='sample', + version='0.0', + description='description', + packages=find_packages() + ) + """) + result = testdir.runpytest(p, '--doctest-modules') + result.stdout.fnmatch_lines(['*collected 0 items*']) + + def test_invalid_setup_py(self, testdir): + ''' + Test to make sure that pytest reads setup.py files that are not used + for python packages when ran with --doctest-modules + ''' + p = testdir.makepyfile(setup=""" + def test_foo(): + return 'bar' + """) + result = testdir.runpytest(p, '--doctest-modules') + result.stdout.fnmatch_lines(['*collected 1 item*']) + class TestLiterals(object): From 843872b501721d53704097931472a33fef1d2813 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 16 Oct 2017 21:07:57 -0200 Subject: [PATCH 109/159] Improve formatting in 502.feature file --- changelog/502.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/502.feature b/changelog/502.feature index f768b650e..eb61640b9 100644 --- a/changelog/502.feature +++ b/changelog/502.feature @@ -1 +1 @@ -Implement feature to skip valid setup.py files when ran with --doctest-modules +Implement feature to skip ``setup.py`` files when ran with ``--doctest-modules``. From 00d3abe6dcb63a94c1b76b4187fdef07510048a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 17 Oct 2017 17:53:47 +0200 Subject: [PATCH 110/159] Adding Failed exception to manage maxfail behavior --- _pytest/main.py | 14 +++++++++++++- changelog/2845.bugfix | 1 + testing/test_collection.py | 6 ++---- testing/test_terminal.py | 1 - 4 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 changelog/2845.bugfix diff --git a/_pytest/main.py b/_pytest/main.py index f05cb7ff3..f7c3fe480 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -102,6 +102,8 @@ def wrap_session(config, doit): session.exitstatus = doit(config, session) or 0 except UsageError: raise + except Failed: + session.exitstatus = EXIT_TESTSFAILED except KeyboardInterrupt: excinfo = _pytest._code.ExceptionInfo() if initstate < 2 and isinstance(excinfo.value, exit.Exception): @@ -159,6 +161,8 @@ def pytest_runtestloop(session): 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 @@ -564,8 +568,13 @@ class Interrupted(KeyboardInterrupt): __module__ = 'builtins' # for py3 +class Failed(Exception): + """ signals an stop as failed test run. """ + + class Session(FSCollector): Interrupted = Interrupted + Failed = Failed def __init__(self, config): FSCollector.__init__(self, config.rootdir, parent=None, @@ -573,6 +582,7 @@ class Session(FSCollector): 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 = py.path.local() @@ -583,6 +593,8 @@ class Session(FSCollector): @hookimpl(tryfirst=True) def pytest_collectstart(self): + if self.shouldfail: + raise self.Failed(self.shouldfail) if self.shouldstop: raise self.Interrupted(self.shouldstop) @@ -592,7 +604,7 @@ class Session(FSCollector): self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") if maxfail and self.testsfailed >= maxfail: - self.shouldstop = "stopping after %d failures" % ( + self.shouldfail = "stopping after %d failures" % ( self.testsfailed) pytest_collectreport = pytest_runtest_logreport diff --git a/changelog/2845.bugfix b/changelog/2845.bugfix new file mode 100644 index 000000000..fffd7a987 --- /dev/null +++ b/changelog/2845.bugfix @@ -0,0 +1 @@ +Change return value of pytest command when maxfail is reached from 2 to 1. diff --git a/testing/test_collection.py b/testing/test_collection.py index 32a336bbd..eb2814527 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -767,12 +767,11 @@ def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir): testdir.makepyfile(**COLLECTION_ERROR_PY_FILES) res = testdir.runpytest("--maxfail=1") - assert res.ret == 2 + assert res.ret == 1 res.stdout.fnmatch_lines([ "*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*", - "*Interrupted: stopping after 1 failures*", ]) assert 'test_03' not in res.stdout.str() @@ -824,10 +823,9 @@ def test_continue_on_collection_errors_maxfail(testdir): testdir.makepyfile(**COLLECTION_ERROR_PY_FILES) res = testdir.runpytest("--continue-on-collection-errors", "--maxfail=3") - assert res.ret == 2 + assert res.ret == 1 res.stdout.fnmatch_lines([ "collected 2 items / 2 errors", - "*Interrupted: stopping after 3 failures*", "*1 failed, 2 error*", ]) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1583a995d..c2a6c0fcf 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -757,7 +757,6 @@ class TestGenericReporting(object): result.stdout.fnmatch_lines([ "*def test_1():*", "*def test_2():*", - "*!! Interrupted: stopping after 2 failures*!!*", "*2 failed*", ]) From e81b275eda947249761583f8611d4fdd14481dc5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 18 Oct 2017 18:54:44 -0200 Subject: [PATCH 111/159] Update formatting in CHANGELOG --- changelog/2845.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2845.bugfix b/changelog/2845.bugfix index fffd7a987..c6767631e 100644 --- a/changelog/2845.bugfix +++ b/changelog/2845.bugfix @@ -1 +1 @@ -Change return value of pytest command when maxfail is reached from 2 to 1. +Change return value of pytest command when ``--maxfail`` is reached from ``2`` (interrupted) to ``1`` (failed). From 5c71151967797b5b4886b7021edabdf17ee6e566 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 19 Oct 2017 20:48:23 -0200 Subject: [PATCH 112/159] Add more text to the 2.6 and 3.3 announcement The text was a bit short for this important announcement. --- changelog/2812.removal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2812.removal b/changelog/2812.removal index c619ee2da..74894e0ef 100644 --- a/changelog/2812.removal +++ b/changelog/2812.removal @@ -1 +1 @@ -remove support for the eol python versions 2.6 and 3.3 \ No newline at end of file +Pytest no longer supports Python **2.6** and **3.3**. Those Python versions are EOL for some time now and incurr maintanance and compatibility costs on the pytest core team, and following up with the rest of the community we decided that they will no longer be supported starting on this version. Users which still require those versions should pin pytest to ``<3.3``. From 3da28067f306582a10b798ba527356d62c1f4f86 Mon Sep 17 00:00:00 2001 From: Ceridwen Date: Thu, 19 Oct 2017 16:01:26 -0700 Subject: [PATCH 113/159] Replace introspection in compat.getfuncargnames() with inspect/funcsigs.signature --- AUTHORS | 1 + _pytest/compat.py | 95 +++++++++++++++++++++------------------ _pytest/fixtures.py | 3 +- changelog/2267.feature | 4 ++ setup.py | 5 ++- testing/python/fixture.py | 5 +-- 6 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 changelog/2267.feature diff --git a/AUTHORS b/AUTHORS index f1769116d..44d11ed32 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,6 +30,7 @@ Brianna Laugher Bruno Oliveira Cal Leeming Carl Friedrich Bolz +Ceridwen Charles Cloud Charnjit SiNGH (CCSJ) Chris Lamb diff --git a/_pytest/compat.py b/_pytest/compat.py index 99ec54c53..8499e8882 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -2,11 +2,12 @@ python version compatibility code """ from __future__ import absolute_import, division, print_function -import sys + +import codecs +import functools import inspect import re -import functools -import codecs +import sys import py @@ -25,6 +26,12 @@ _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 + + NoneType = type(None) NOTSET = object() @@ -32,12 +39,10 @@ PY35 = sys.version_info[:2] >= (3, 5) PY36 = sys.version_info[:2] >= (3, 6) MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError' -if hasattr(inspect, 'signature'): - def _format_args(func): - return str(inspect.signature(func)) -else: - def _format_args(func): - return inspect.formatargspec(*inspect.getargspec(func)) + +def _format_args(func): + return str(signature(func)) + isfunction = inspect.isfunction isclass = inspect.isclass @@ -63,7 +68,6 @@ def iscoroutinefunction(func): def getlocation(function, curdir): - import inspect fn = py.path.local(inspect.getfile(function)) lineno = py.builtin._getcode(function).co_firstlineno if fn.relto(curdir): @@ -83,40 +87,45 @@ def num_mock_patch_args(function): return len(patchings) -def getfuncargnames(function, startindex=None, cls=None): +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. + """ - @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. - """ - if startindex is None and cls is not None: - is_staticmethod = isinstance(cls.__dict__.get(function.__name__, None), staticmethod) - startindex = 0 if is_staticmethod else 1 - # XXX merge with main.py's varnames - # assert not isclass(function) - realfunction = function - while hasattr(realfunction, "__wrapped__"): - realfunction = realfunction.__wrapped__ - if startindex is None: - startindex = inspect.ismethod(function) and 1 or 0 - if realfunction != function: - startindex += num_mock_patch_args(function) - function = realfunction - if isinstance(function, functools.partial): - argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0] - partial = function - argnames = argnames[len(partial.args):] - if partial.keywords: - for kw in partial.keywords: - argnames.remove(kw) - else: - argnames = inspect.getargs(_pytest._code.getrawcode(function))[0] - defaults = getattr(function, 'func_defaults', - getattr(function, '__defaults__', None)) or () - numdefaults = len(defaults) - if numdefaults: - return tuple(argnames[startindex:-numdefaults]) - return tuple(argnames[startindex:]) + # 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. + arg_names = tuple( + p.name for p in signature(function).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 if _PY3: diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index af993f3f9..5ac93b1a9 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -728,8 +728,7 @@ class FixtureDef: where=baseid ) self.params = params - startindex = unittest and 1 or None - self.argnames = getfuncargnames(func, startindex=startindex) + self.argnames = getfuncargnames(func, is_method=unittest) self.unittest = unittest self.ids = ids self._finalizer = [] diff --git a/changelog/2267.feature b/changelog/2267.feature new file mode 100644 index 000000000..a2f14811e --- /dev/null +++ b/changelog/2267.feature @@ -0,0 +1,4 @@ +Replace the old introspection code in compat.py that determines the +available arguments of fixtures with inspect.signature on Python 3 and +funcsigs.signature on Python 2. This should respect __signature__ +declarations on functions. diff --git a/setup.py b/setup.py index 68b8ec065..61ae1587f 100644 --- a/setup.py +++ b/setup.py @@ -44,16 +44,19 @@ def has_environment_marker_support(): def main(): install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools'] + extras_require = {} # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ: install_requires.append('pluggy>=0.4.0,<0.5') - extras_require = {} if has_environment_marker_support(): + extras_require[':python_version<"3.0"'] = ['funcsigs'] extras_require[':sys_platform=="win32"'] = ['colorama'] else: if sys.platform == 'win32': install_requires.append('colorama') + if sys.version_info < (3, 0): + install_requires.append('funcsigs') setup( name='pytest', diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 06b08d68e..fa5da3284 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -34,9 +34,6 @@ def test_getfuncargnames(): pass assert fixtures.getfuncargnames(A().f) == ('arg1',) - if sys.version_info < (3, 0): - assert fixtures.getfuncargnames(A.f) == ('arg1',) - assert fixtures.getfuncargnames(A.static, cls=A) == ('arg1', 'arg2') @@ -2826,7 +2823,7 @@ class TestShowFixtures(object): import pytest class TestClass: @pytest.fixture - def fixture1(): + def fixture1(self): """line1 line2 indented line From f7387e45ea0b163ba4944801fdb70c521d7b8cc8 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 19 Oct 2017 21:50:15 -0200 Subject: [PATCH 114/159] Fix linting --- testing/python/fixture.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index fa5da3284..184a80374 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2,7 +2,6 @@ from textwrap import dedent import _pytest._code import pytest -import sys from _pytest.pytester import get_public_names from _pytest.fixtures import FixtureLookupError from _pytest import fixtures From 6b86b0dbfea3895a1d16d7db970d3ea91de92ecc Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 24 Oct 2017 21:01:00 -0200 Subject: [PATCH 115/159] Fix additional linting issues --- _pytest/compat.py | 12 ++++-------- _pytest/pytester.py | 4 +--- _pytest/recwarn.py | 6 ++---- testing/logging/test_fixture.py | 2 +- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/_pytest/compat.py b/_pytest/compat.py index 8499e8882..263305861 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -14,7 +14,6 @@ import py import _pytest from _pytest.outcomes import TEST_OUTCOME - try: import enum except ImportError: # pragma: no cover @@ -110,11 +109,10 @@ def getfuncargnames(function, is_method=False, cls=None): # ordered mapping of parameter names to Parameter instances. This # creates a tuple of the names of the parameters that don't have # defaults. - arg_names = tuple( - p.name for p in signature(function).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 signature(function).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. @@ -173,8 +171,6 @@ else: STRING_TYPES = bytes, str, unicode UNICODE_TYPES = unicode, - from itertools import imap, izip # NOQA - 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, diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 345a1acd0..a65e3f027 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -23,9 +23,7 @@ from _pytest.main import Session, EXIT_OK from _pytest.assertion.rewrite import AssertionRewritingHook -PYTEST_FULLPATH = os.path.abspath( - pytest.__file__.rstrip("oc") - ).replace("$py.class", ".py") +PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py") def pytest_addoption(parser): diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index c9f86a483..4fceb10a7 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -232,7 +232,5 @@ class WarningsChecker(WarningsRecorder): else: fail("DID NOT WARN. No warnings of type {0} matching" " ('{1}') was emitted. The list of emitted warnings" - " is: {2}.".format( - self.expected_warning, - self.match_expr, - [each.message for each in self])) + " is: {2}.".format(self.expected_warning, self.match_expr, + [each.message for each in self])) diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index b5bee4233..c27b31137 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -3,7 +3,7 @@ import logging logger = logging.getLogger(__name__) -sublogger = logging.getLogger(__name__+'.baz') +sublogger = logging.getLogger(__name__ + '.baz') def test_fixture_help(testdir): From 4e581b637f3082836a0c711b9fb62dac485cca79 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 24 Oct 2017 22:13:32 -0200 Subject: [PATCH 116/159] Use zip and map from six --- _pytest/compat.py | 2 -- _pytest/mark.py | 4 ++-- _pytest/python_api.py | 5 +++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/_pytest/compat.py b/_pytest/compat.py index 263305861..7560fbec3 100644 --- a/_pytest/compat.py +++ b/_pytest/compat.py @@ -127,8 +127,6 @@ def getfuncargnames(function, is_method=False, cls=None): if _PY3: - imap = map - izip = zip STRING_TYPES = bytes, str UNICODE_TYPES = str, diff --git a/_pytest/mark.py b/_pytest/mark.py index f4058989a..03b058d95 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -5,7 +5,7 @@ import inspect import warnings from collections import namedtuple from operator import attrgetter -from .compat import imap +from six.moves import map from .deprecated import MARK_PARAMETERSET_UNPACKING @@ -427,7 +427,7 @@ class MarkInfo(object): def __iter__(self): """ yield MarkInfo objects each relating to a marking-call. """ - return imap(MarkInfo, self._marks) + return map(MarkInfo, self._marks) MARK_GEN = MarkGenerator() diff --git a/_pytest/python_api.py b/_pytest/python_api.py index 6ae3e81b6..bf1cd147e 100644 --- a/_pytest/python_api.py +++ b/_pytest/python_api.py @@ -2,8 +2,9 @@ import math import sys import py +from six.moves import zip -from _pytest.compat import isclass, izip +from _pytest.compat import isclass from _pytest.outcomes import fail import _pytest._code @@ -145,7 +146,7 @@ class ApproxSequence(ApproxBase): return ApproxBase.__eq__(self, actual) def _yield_comparisons(self, actual): - return izip(actual, self.expected) + return zip(actual, self.expected) class ApproxScalar(ApproxBase): From 4a436f225532e019787f5ef88be949e826928e21 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 27 Oct 2017 17:52:14 +0200 Subject: [PATCH 117/159] move responsibility for parameterset extraction into parameterset class --- _pytest/mark.py | 25 +++++++++++++++++++++++++ _pytest/python.py | 24 +++--------------------- changelog/2877.trivial | 1 + 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 changelog/2877.trivial diff --git a/_pytest/mark.py b/_pytest/mark.py index 03b058d95..a5c972e5d 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -7,6 +7,7 @@ from collections import namedtuple from operator import attrgetter from six.moves import map from .deprecated import MARK_PARAMETERSET_UNPACKING +from .compat import NOTSET, getfslineno def alias(name, warning=None): @@ -67,6 +68,30 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')): return cls(argval, marks=newmarks, id=None) + @classmethod + def _for_parameterize(cls, argnames, argvalues, function): + 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 + parameters = [ + ParameterSet.extract_from(x, legacy_force_tuple=force_tuple) + for x in argvalues] + del argvalues + + if not parameters: + fs, lineno = getfslineno(function) + reason = "got empty parameter set %r, function %s at %s:%d" % ( + argnames, function.__name__, fs, lineno) + mark = MARK_GEN.skip(reason=reason) + parameters.append(ParameterSet( + values=(NOTSET,) * len(argnames), + marks=[mark], + id=None, + )) + return argnames, parameters + class MarkerError(Exception): diff --git a/_pytest/python.py b/_pytest/python.py index c47422937..83e8dad9a 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -769,30 +769,12 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): to set a dynamic scope using test context or configuration. """ from _pytest.fixtures import scope2index - from _pytest.mark import MARK_GEN, ParameterSet + from _pytest.mark import ParameterSet from py.io import saferepr - - 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 - parameters = [ - ParameterSet.extract_from(x, legacy_force_tuple=force_tuple) - for x in argvalues] + argnames, parameters = ParameterSet._for_parameterize( + argnames, argvalues, self.function) del argvalues - if not parameters: - fs, lineno = getfslineno(self.function) - reason = "got empty parameter set %r, function %s at %s:%d" % ( - argnames, self.function.__name__, fs, lineno) - mark = MARK_GEN.skip(reason=reason) - parameters.append(ParameterSet( - values=(NOTSET,) * len(argnames), - marks=[mark], - id=None, - )) - if scope is None: scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) diff --git a/changelog/2877.trivial b/changelog/2877.trivial new file mode 100644 index 000000000..aaf58b039 --- /dev/null +++ b/changelog/2877.trivial @@ -0,0 +1 @@ +internal move of the parameterset extraction to a more maintainable place \ No newline at end of file From b27dde24d6fc4e737c685effe18ae121881c976c Mon Sep 17 00:00:00 2001 From: Samuel Dion-Girardeau Date: Sat, 28 Oct 2017 14:53:19 -0400 Subject: [PATCH 118/159] Use a nametuple for `readouterr()` results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows accessing `out` and `err` directly by attribute, while preserving tuple unpacking. Also added tests, one for the `capsys` fixture, and one for the `MultiCapture` class itself. --- _pytest/capture.py | 8 ++++++-- testing/test_capture.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/_pytest/capture.py b/_pytest/capture.py index 13e1216cc..f6d3c61b3 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -4,6 +4,7 @@ per-test stdout/stderr capturing mechanism. """ from __future__ import absolute_import, division, print_function +import collections import contextlib import sys import os @@ -306,6 +307,9 @@ class EncodedFile(object): return getattr(object.__getattribute__(self, "buffer"), name) +CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"]) + + class MultiCapture(object): out = err = in_ = None @@ -366,8 +370,8 @@ class MultiCapture(object): def readouterr(self): """ return snapshot unicode value of stdout/stderr capturings. """ - return (self.out.snap() if self.out is not None else "", - self.err.snap() if self.err is not None else "") + return CaptureResult(self.out.snap() if self.out is not None else "", + self.err.snap() if self.err is not None else "") class NoCapture: diff --git a/testing/test_capture.py b/testing/test_capture.py index f961694ff..a21e767a8 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -922,6 +922,14 @@ class TestStdCapture(object): out, err = cap.readouterr() assert err == "error2" + def test_capture_results_accessible_by_attribute(self): + with self.getcapture() as cap: + sys.stdout.write("hello") + sys.stderr.write("world") + capture_result = cap.readouterr() + assert capture_result.out == "hello" + assert capture_result.err == "world" + def test_capturing_readouterr_unicode(self): with self.getcapture() as cap: print("hx\xc4\x85\xc4\x87") @@ -1083,6 +1091,14 @@ def test_using_capsys_fixture_works_with_sys_stdout_encoding(capsys): assert err == '' +def test_capsys_results_accessible_by_attribute(capsys): + sys.stdout.write("spam") + sys.stderr.write("eggs") + capture_result = capsys.readouterr() + assert capture_result.out == "spam" + assert capture_result.err == "eggs" + + @needsosdup @pytest.mark.parametrize('use', [True, False]) def test_fdcapture_tmpfile_remains_the_same(tmpfile, use): From 8e28815d4455dbfcd4bcec8ba3c0f096dbcb1abd Mon Sep 17 00:00:00 2001 From: Samuel Dion-Girardeau Date: Sat, 28 Oct 2017 15:07:26 -0400 Subject: [PATCH 119/159] Add changelog entry for issue #2879 --- changelog/2879.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2879.feature diff --git a/changelog/2879.feature b/changelog/2879.feature new file mode 100644 index 000000000..8932d8c30 --- /dev/null +++ b/changelog/2879.feature @@ -0,0 +1 @@ +Return stdout/stderr capture results as a ``namedtuple``, so ``out`` and ``err`` can be accessed by attribute. From 8e178e9f9b5d8ae217fcd7635651157c5c9a94bc Mon Sep 17 00:00:00 2001 From: Samuel Dion-Girardeau Date: Sat, 28 Oct 2017 15:08:53 -0400 Subject: [PATCH 120/159] Add myself to AUTHORS list --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index aeef6dd9b..ea3b7f50e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -156,6 +156,7 @@ Ronny Pfannschmidt Ross Lawley Russel Winder Ryan Wooden +Samuel Dion-Girardeau Samuele Pedroni Segev Finer Simon Gomizelj From 821f9a94d8e5f8328abb43277781d29c03fa1b05 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 27 Oct 2017 18:34:52 +0200 Subject: [PATCH 121/159] deprecate the public internal PyCollector.makeitem method --- _pytest/deprecated.py | 5 +++++ _pytest/python.py | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index 38e949677..e9231f221 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -40,3 +40,8 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( " please use pytest.param(..., marks=...) instead.\n" "For more details, see: https://docs.pytest.org/en/latest/parametrize.html" ) + +COLLECTOR_MAKEITEM = RemovedInPytest4Warning( + "pycollector makeitem was removed " + "as it is an accidentially leaked internal api" +) \ No newline at end of file diff --git a/_pytest/python.py b/_pytest/python.py index c47422937..4a0501bf4 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -6,9 +6,11 @@ import inspect import sys import os import collections +import warnings from textwrap import dedent from itertools import count + import py import six from _pytest.mark import MarkerError @@ -18,6 +20,7 @@ import _pytest import pluggy from _pytest import fixtures from _pytest import main +from _pytest import deprecated from _pytest.compat import ( isclass, isfunction, is_generator, ascii_escaped, REGEX_TYPE, STRING_TYPES, NoneType, NOTSET, @@ -328,7 +331,7 @@ class PyCollector(PyobjMixin, main.Collector): if name in seen: continue seen[name] = True - res = self.makeitem(name, obj) + res = self._makeitem(name, obj) if res is None: continue if not isinstance(res, list): @@ -338,6 +341,10 @@ class PyCollector(PyobjMixin, main.Collector): return l def makeitem(self, name, obj): + warnings.warn(deprecated.COLLECTOR_MAKEITEM, stacklevel=2) + self._makeitem(name, obj) + + def _makeitem(self, name, obj): # assert self.ihook.fspath == self.fspath, self return self.ihook.pytest_pycollect_makeitem( collector=self, name=name, obj=obj) From 766de67392dda354fd37023f219dd71a4252efce Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 27 Oct 2017 18:42:46 -0200 Subject: [PATCH 122/159] Fix linting error in deprecated.py --- _pytest/deprecated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index e9231f221..910510b01 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -44,4 +44,4 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning( COLLECTOR_MAKEITEM = RemovedInPytest4Warning( "pycollector makeitem was removed " "as it is an accidentially leaked internal api" -) \ No newline at end of file +) From 07b2b18a01a3ba12757a4eb1c0e082922f5299e5 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Tue, 1 Aug 2017 11:52:09 +0200 Subject: [PATCH 123/159] introduce attrs as dependency and use it for FixtureFunctionMarker and marks --- _pytest/fixtures.py | 23 ++++++++++++++------- _pytest/mark.py | 50 ++++++++++++++++++++++++++------------------- setup.py | 7 ++++++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 596354ee3..c5da9f4cf 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -7,6 +7,7 @@ import warnings import py from py._code.code import FormattedExcinfo +import attr import _pytest from _pytest import nodes from _pytest._code.code import TerminalRepr @@ -822,13 +823,21 @@ def pytest_fixture_setup(fixturedef, request): return result -class FixtureFunctionMarker: - def __init__(self, scope, params, autouse=False, ids=None, name=None): - self.scope = scope - self.params = params - self.autouse = autouse - self.ids = ids - self.name = name +def _ensure_immutable_ids(ids): + if ids is None: + return + if callable(ids): + return ids + return tuple(ids) + + +@attr.s(frozen=True) +class FixtureFunctionMarker(object): + scope = attr.ib() + params = attr.ib(convert=attr.converters.optional(tuple)) + autouse = attr.ib(default=False) + ids = attr.ib(default=None, convert=_ensure_immutable_ids) + name = attr.ib(default=None) def __call__(self, function): if isclass(function): diff --git a/_pytest/mark.py b/_pytest/mark.py index 03b058d95..7852f281c 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function import inspect import warnings +import attr from collections import namedtuple from operator import attrgetter from six.moves import map @@ -160,22 +161,26 @@ def pytest_collection_modifyitems(items, config): items[:] = remaining -class MarkMapping: +@attr.s +class MarkMapping(object): """Provides a local mapping for markers where item access resolves to True if the marker is present. """ - def __init__(self, keywords): - mymarks = set() + own_mark_names = attr.ib() + + @classmethod + def from_keywords(cls, keywords): + mark_names = set() for key, value in keywords.items(): if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator): - mymarks.add(key) - self._mymarks = mymarks + mark_names.add(key) + return cls(mark_names) def __getitem__(self, name): - return name in self._mymarks + return name in self.own_mark_names -class KeywordMapping: +class KeywordMapping(object): """Provides a local mapping for keywords. Given a list of names, map any substring of one of these names to True. """ @@ -192,7 +197,7 @@ class KeywordMapping: def matchmark(colitem, markexpr): """Tries to match on any marker names, attached to the given colitem.""" - return eval(markexpr, {}, MarkMapping(colitem.keywords)) + return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords)) def matchkeyword(colitem, keywordexpr): @@ -280,7 +285,21 @@ def istestfunc(func): getattr(func, "__name__", "") != "" -class MarkDecorator: +@attr.s(frozen=True) +class Mark(object): + name = attr.ib() + args = attr.ib() + kwargs = attr.ib() + + def combined_with(self, other): + 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 `. @@ -314,9 +333,7 @@ class MarkDecorator: """ - def __init__(self, mark): - assert isinstance(mark, Mark), repr(mark) - self.mark = mark + mark = attr.ib(validator=attr.validators.instance_of(Mark)) name = alias('mark.name') args = alias('mark.args') @@ -396,15 +413,6 @@ def store_legacy_markinfo(func, mark): holder.add_mark(mark) -class Mark(namedtuple('Mark', 'name, args, kwargs')): - - def combined_with(self, other): - assert self.name == other.name - return Mark( - self.name, self.args + other.args, - dict(self.kwargs, **other.kwargs)) - - class MarkInfo(object): """ Marking object created by :class:`MarkDecorator` instances. """ diff --git a/setup.py b/setup.py index 61ae1587f..c4fc48920 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,13 @@ def has_environment_marker_support(): def main(): - install_requires = ['py>=1.4.34', 'six>=1.10.0', 'setuptools'] extras_require = {} + install_requires = [ + 'py>=1.4.33', + 'six>=1.10.0', + 'setuptools', + 'attrs>=17.2.0', + ] # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ: From d1aa553f739e91cd470eea23042b6c8bcebe9b6f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 30 Oct 2017 16:44:49 +0100 Subject: [PATCH 124/159] add mocked integrationtest for the deprecationwarning of makeitem --- testing/python/test_deprecations.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 testing/python/test_deprecations.py diff --git a/testing/python/test_deprecations.py b/testing/python/test_deprecations.py new file mode 100644 index 000000000..5001f765f --- /dev/null +++ b/testing/python/test_deprecations.py @@ -0,0 +1,22 @@ +import pytest + +from _pytest.python import PyCollector + + +class PyCollectorMock(PyCollector): + """evil hack""" + + def __init__(self): + self.called = False + + def _makeitem(self, *k): + """hack to disable the actual behaviour""" + self.called = True + + +def test_pycollector_makeitem_is_deprecated(): + + collector = PyCollectorMock() + with pytest.deprecated_call(): + collector.makeitem('foo', 'bar') + assert collector.called From f3a119c06a8197ac986241e800beabfa63725f54 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Nov 2017 16:37:18 -0200 Subject: [PATCH 125/159] Merge upstream/master into features --- AUTHORS | 1 + _pytest/assertion/rewrite.py | 16 ++++++--- _pytest/doctest.py | 2 +- _pytest/mark.py | 5 +-- changelog/1505.doc | 1 + changelog/2658.doc | 1 + changelog/2856.bugfix | 1 + changelog/2882.bugfix | 1 + doc/en/assert.rst | 4 +-- doc/en/example/parametrize.rst | 50 +++++++++++++++++++++++++++++ doc/en/example/pythoncollection.rst | 30 +++++++++-------- doc/en/fixture.rst | 44 +++++++++++++++++-------- doc/en/plugins.rst | 3 +- doc/en/skipping.rst | 12 ++++++- doc/en/writing_plugins.rst | 2 -- testing/test_assertrewrite.py | 48 +++++++++++++++++++-------- testing/test_doctest.py | 30 +++++++++++++++-- testing/test_mark.py | 17 ++++++++++ tox.ini | 6 ++-- 19 files changed, 215 insertions(+), 59 deletions(-) create mode 100644 changelog/1505.doc create mode 100644 changelog/2658.doc create mode 100644 changelog/2856.bugfix create mode 100644 changelog/2882.bugfix diff --git a/AUTHORS b/AUTHORS index ea3b7f50e..34c6f6437 100644 --- a/AUTHORS +++ b/AUTHORS @@ -47,6 +47,7 @@ Dave Hunt David Díaz-Barquero David Mohr David Vierra +Daw-Ran Liou Denis Kirisov Diego Russo Dmitry Dygalo diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 6800f82e6..34fc0c8f6 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -591,23 +591,26 @@ class AssertionRewriter(ast.NodeVisitor): # docstrings and __future__ imports. aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] - expect_docstring = True + 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 = 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 "PYTEST_DONT_REWRITE" in doc: - # The module has disabled assertion rewriting. + if self.is_rewrite_disabled(doc): return - lineno += len(doc) - 1 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 @@ -633,6 +636,9 @@ class AssertionRewriter(ast.NodeVisitor): not isinstance(field, ast.expr)): nodes.append(field) + def is_rewrite_disabled(self, docstring): + return "PYTEST_DONT_REWRITE" in docstring + def variable(self): """Get a new variable.""" # Use a character invalid in python identifiers to avoid clashing. diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 6016265a5..bba90e551 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -127,7 +127,7 @@ class DoctestItem(pytest.Item): lines = ["%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)] # trim docstring error lines to 10 - lines = lines[example.lineno - 9:example.lineno + 1] + lines = lines[max(example.lineno - 9, 0):example.lineno + 1] else: lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example'] indent = '>>>' diff --git a/_pytest/mark.py b/_pytest/mark.py index 03b058d95..8879b9fff 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -268,8 +268,9 @@ class MarkGenerator: pass self._markers = l = set() for line in self._config.getini("markers"): - beginning = line.split(":", 1) - x = beginning[0].split("(", 1)[0] + marker, _ = line.split(":", 1) + marker = marker.rstrip() + x = marker.split("(", 1)[0] l.add(x) if name not in self._markers: raise AttributeError("%r not a registered marker" % (name,)) diff --git a/changelog/1505.doc b/changelog/1505.doc new file mode 100644 index 000000000..1b303d1bd --- /dev/null +++ b/changelog/1505.doc @@ -0,0 +1 @@ +Introduce a dedicated section about conftest.py. diff --git a/changelog/2658.doc b/changelog/2658.doc new file mode 100644 index 000000000..2da7f3d6c --- /dev/null +++ b/changelog/2658.doc @@ -0,0 +1 @@ +Append example for pytest.param in the example/parametrize document. \ No newline at end of file diff --git a/changelog/2856.bugfix b/changelog/2856.bugfix new file mode 100644 index 000000000..7e5fc8fc7 --- /dev/null +++ b/changelog/2856.bugfix @@ -0,0 +1 @@ +Strip whitespace from marker names when reading them from INI config. diff --git a/changelog/2882.bugfix b/changelog/2882.bugfix new file mode 100644 index 000000000..2bda24c01 --- /dev/null +++ b/changelog/2882.bugfix @@ -0,0 +1 @@ +Show full context of doctest source in the pytest output, if the lineno of failed example in the docstring is < 9. \ No newline at end of file diff --git a/doc/en/assert.rst b/doc/en/assert.rst index a8ddaecd8..d9e044356 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -209,8 +209,8 @@ the ``pytest_assertrepr_compare`` hook. .. autofunction:: _pytest.hookspec.pytest_assertrepr_compare :noindex: -As an example consider adding the following hook in a conftest.py which -provides an alternative explanation for ``Foo`` objects:: +As an example consider adding the following hook in a :ref:`conftest.py ` +file which provides an alternative explanation for ``Foo`` objects:: # content of conftest.py from test_foocompare import Foo diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index ffeb5a951..1a8de235a 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -485,4 +485,54 @@ of our ``test_func1`` was skipped. A few notes: values as well. +Set marks or test ID for individual parametrized test +-------------------------------------------------------------------- +Use ``pytest.param`` to apply marks or set test ID to individual parametrized test. +For example:: + + # content of test_pytest_param_example.py + import pytest + @pytest.mark.parametrize('test_input,expected', [ + ('3+5', 8), + pytest.param('1+7', 8, + marks=pytest.mark.basic), + pytest.param('2+4', 6, + marks=pytest.mark.basic, + id='basic_2+4'), + pytest.param('6*9', 42, + marks=[pytest.mark.basic, pytest.mark.xfail], + id='basic_6*9'), + ]) + def test_eval(test_input, expected): + assert eval(test_input) == expected + +In this example, we have 4 parametrized tests. Except for the first test, +we mark the rest three parametrized tests with the custom marker ``basic``, +and for the fourth test we also use the built-in mark ``xfail`` to indicate this +test is expected to fail. For explicitness, we set test ids for some tests. + +Then run ``pytest`` with verbose mode and with only the ``basic`` marker:: + + pytest -v -m basic + ============================================ test session starts ============================================= + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y + rootdir: $REGENDOC_TMPDIR, inifile: + collected 4 items + + test_pytest_param_example.py::test_eval[1+7-8] PASSED + test_pytest_param_example.py::test_eval[basic_2+4] PASSED + test_pytest_param_example.py::test_eval[basic_6*9] xfail + ========================================== short test summary info =========================================== + XFAIL test_pytest_param_example.py::test_eval[basic_6*9] + + ============================================= 1 tests deselected ============================================= + +As the result: + +- Four tests were collected +- One test was deselected because it doesn't have the ``basic`` mark. +- Three tests with the ``basic`` mark was selected. +- The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing. +- The test ``test_eval[basic_2+4]`` passed. +- The test ``test_eval[basic_6*9]`` was expected to fail and did fail. diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 8d36c2e37..5fb63035a 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -175,21 +175,23 @@ You can always peek at the collection tree without running tests like this:: ======= no tests ran in 0.12 seconds ======== -customizing test collection to find all .py files ---------------------------------------------------------- +.. _customizing-test-collection: + +Customizing test collection +--------------------------- .. regendoc:wipe -You can easily instruct ``pytest`` to discover tests from every python file:: - +You can easily instruct ``pytest`` to discover tests from every Python file:: # content of pytest.ini [pytest] python_files = *.py -However, many projects will have a ``setup.py`` which they don't want to be imported. Moreover, there may files only importable by a specific python version. -For such cases you can dynamically define files to be ignored by listing -them in a ``conftest.py`` file:: +However, many projects will have a ``setup.py`` which they don't want to be +imported. Moreover, there may files only importable by a specific python +version. For such cases you can dynamically define files to be ignored by +listing them in a ``conftest.py`` file:: # content of conftest.py import sys @@ -198,7 +200,7 @@ them in a ``conftest.py`` file:: if sys.version_info[0] > 2: collect_ignore.append("pkg/module_py2.py") -And then if you have a module file like this:: +and then if you have a module file like this:: # content of pkg/module_py2.py def test_only_on_python2(): @@ -207,13 +209,13 @@ And then if you have a module file like this:: except Exception, e: pass -and a setup.py dummy file like this:: +and a ``setup.py`` dummy file like this:: # content of setup.py 0/0 # will raise exception if imported -then a pytest run on Python2 will find the one test and will leave out the -setup.py file:: +If you run with a Python 2 interpreter then you will find the one test and will +leave out the ``setup.py`` file:: #$ pytest --collect-only ====== test session starts ====== @@ -225,13 +227,13 @@ setup.py file:: ====== no tests ran in 0.04 seconds ====== -If you run with a Python3 interpreter both the one test and the setup.py file -will be left out:: +If you run with a Python 3 interpreter both the one test and the ``setup.py`` +file will be left out:: $ pytest --collect-only ======= test session starts ======== platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items - + ======= no tests ran in 0.12 seconds ======== diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index dace0514e..1d7ba8640 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -127,10 +127,39 @@ It's a prime example of `dependency injection`_ where fixture functions take the role of the *injector* and test functions are the *consumers* of fixture objects. +.. _`conftest.py`: +.. _`conftest`: + +``conftest.py``: sharing fixture functions +------------------------------------------ + +If during implementing your tests you realize that you +want to use a fixture function from multiple test files you can move it +to a ``conftest.py`` file. +You don't need to import the fixture you want to use in a test, it +automatically gets discovered by pytest. The discovery of +fixture functions starts at test classes, then test modules, then +``conftest.py`` files and finally builtin and third party plugins. + +You can also use the ``conftest.py`` file to implement +:ref:`local per-directory plugins `. + +Sharing test data +----------------- + +If you want to make test data from files available to your tests, a good way +to do this is by loading these data in a fixture for use by your tests. +This makes use of the automatic caching mechanisms of pytest. + +Another good approach is by adding the data files in the ``tests`` folder. +There are also community plugins available to help managing this aspect of +testing, e.g. `pytest-datadir `__ +and `pytest-datafiles `__. + .. _smtpshared: -Scope: Sharing a fixture across tests in a class, module or session -------------------------------------------------------------------- +Scope: sharing a fixture instance across tests in a class, module or session +---------------------------------------------------------------------------- .. regendoc:wipe @@ -878,17 +907,6 @@ All test methods in this TestClass will use the transaction fixture while other test classes or functions in the module will not use it unless they also add a ``transact`` reference. - -Shifting (visibility of) fixture functions ----------------------------------------------------- - -If during implementing your tests you realize that you -want to use a fixture function from multiple test files you can move it -to a :ref:`conftest.py ` file or even separately installable -:ref:`plugins ` without changing test code. The discovery of -fixtures functions starts at test classes, then test modules, then -``conftest.py`` files and finally builtin and third party plugins. - Overriding fixtures on various levels ------------------------------------- diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index bba7d3ecd..400418aee 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -91,7 +91,7 @@ environment you can type:: and will get an extended test header which shows activated plugins and their names. It will also print local plugins aka -:ref:`conftest.py ` files when they are loaded. +:ref:`conftest.py ` files when they are loaded. .. _`cmdunregister`: @@ -152,4 +152,3 @@ in the `pytest repository `_. _pytest.terminal _pytest.tmpdir _pytest.unittest - diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index dbe9c7f8d..3159d2083 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -3,7 +3,7 @@ .. _skipping: Skip and xfail: dealing with tests that cannot succeed -===================================================================== +====================================================== You can mark test functions that cannot be run on certain platforms or that you expect to fail so pytest can deal with them accordingly and @@ -152,6 +152,16 @@ will be skipped if any of the skip conditions is true. .. _`whole class- or module level`: mark.html#scoped-marking +Skipping files or directories +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you may need to skip an entire file or directory, for example if the +tests rely on Python version-specific features or contain code that you do not +wish pytest to run. In this case, you must exclude the files and directories +from collection. Refer to :ref:`customizing-test-collection` for more +information. + + Skipping on a missing import dependency ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 5f151b4bb..53a14cb0d 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -57,9 +57,7 @@ Plugin discovery order at tool startup .. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/ .. _`conftest.py plugins`: -.. _`conftest.py`: .. _`localplugin`: -.. _`conftest`: .. _`local conftest plugins`: conftest.py: local per-directory plugins diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 07912f0b7..467f231a6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -65,13 +65,18 @@ class TestAssertionRewrite(object): def test_place_initial_imports(self): s = """'Doc string'\nother = stuff""" m = rewrite(s) - assert isinstance(m.body[0], ast.Expr) - assert isinstance(m.body[0].value, ast.Str) - for imp in m.body[1:3]: + # Module docstrings in 3.7 are part of Module node, it's not in the body + # so we remove it so the following body items have the same indexes on + # all Python versions + if sys.version_info < (3, 7): + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + for imp in m.body[0:2]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[3], ast.Assign) + assert isinstance(m.body[2], ast.Assign) s = """from __future__ import with_statement\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) @@ -80,16 +85,29 @@ class TestAssertionRewrite(object): assert imp.lineno == 2 assert imp.col_offset == 0 assert isinstance(m.body[3], ast.Expr) + s = """'doc string'\nfrom __future__ import with_statement""" + m = rewrite(s) + if sys.version_info < (3, 7): + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + assert isinstance(m.body[0], ast.ImportFrom) + for imp in m.body[1:3]: + assert isinstance(imp, ast.Import) + assert imp.lineno == 2 + assert imp.col_offset == 0 s = """'doc string'\nfrom __future__ import with_statement\nother""" m = rewrite(s) - assert isinstance(m.body[0], ast.Expr) - assert isinstance(m.body[0].value, ast.Str) - assert isinstance(m.body[1], ast.ImportFrom) - for imp in m.body[2:4]: + if sys.version_info < (3, 7): + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + assert isinstance(m.body[0], ast.ImportFrom) + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 3 assert imp.col_offset == 0 - assert isinstance(m.body[4], ast.Expr) + assert isinstance(m.body[3], ast.Expr) s = """from . import relative\nother_stuff""" m = rewrite(s) for imp in m.body[0:2]: @@ -101,10 +119,14 @@ class TestAssertionRewrite(object): def test_dont_rewrite(self): s = """'PYTEST_DONT_REWRITE'\nassert 14""" m = rewrite(s) - assert len(m.body) == 2 - assert isinstance(m.body[0].value, ast.Str) - assert isinstance(m.body[1], ast.Assert) - assert m.body[1].msg is None + if sys.version_info < (3, 7): + assert len(m.body) == 2 + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + else: + assert len(m.body) == 1 + assert m.body[0].msg is None def test_name(self): def f(): diff --git a/testing/test_doctest.py b/testing/test_doctest.py index b8fa1fb77..b15067f15 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -173,7 +173,7 @@ class TestDoctests(object): "*UNEXPECTED*ZeroDivision*", ]) - def test_docstring_context_around_error(self, testdir): + def test_docstring_partial_context_around_error(self, testdir): """Test that we show some context before the actual line of a failing doctest. """ @@ -199,7 +199,7 @@ class TestDoctests(object): ''') result = testdir.runpytest('--doctest-modules') result.stdout.fnmatch_lines([ - '*docstring_context_around_error*', + '*docstring_partial_context_around_error*', '005*text-line-3', '006*text-line-4', '013*text-line-11', @@ -213,6 +213,32 @@ class TestDoctests(object): assert 'text-line-2' not in result.stdout.str() assert 'text-line-after' not in result.stdout.str() + def test_docstring_full_context_around_error(self, testdir): + """Test that we show the whole context before the actual line of a failing + doctest, provided that the context is up to 10 lines long. + """ + testdir.makepyfile(''' + def foo(): + """ + text-line-1 + text-line-2 + + >>> 1 + 1 + 3 + """ + ''') + result = testdir.runpytest('--doctest-modules') + result.stdout.fnmatch_lines([ + '*docstring_full_context_around_error*', + '003*text-line-1', + '004*text-line-2', + '006*>>> 1 + 1', + 'Expected:', + ' 3', + 'Got:', + ' 2', + ]) + def test_doctest_linedata_missing(self, testdir): testdir.tmpdir.join('hello.py').write(_pytest._code.Source(""" class Fun(object): diff --git a/testing/test_mark.py b/testing/test_mark.py index dc51bbac0..9ae88a665 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -169,6 +169,23 @@ def test_markers_option(testdir): ]) +def test_ini_markers_whitespace(testdir): + testdir.makeini(""" + [pytest] + markers = + a1 : this is a whitespace marker + """) + testdir.makepyfile(""" + import pytest + + @pytest.mark.a1 + def test_markers(): + assert True + """) + rec = testdir.inline_run("--strict", "-m", "a1") + rec.assertoutcome(passed=1) + + def test_markers_option_with_plugin_in_current_dir(testdir): testdir.makeconftest('pytest_plugins = "flip_flop"') testdir.makepyfile(flip_flop="""\ diff --git a/tox.ini b/tox.ini index aaf39026b..09f59f5e3 100644 --- a/tox.ini +++ b/tox.ini @@ -54,8 +54,9 @@ deps = mock nose hypothesis>=3.5.2 +changedir=testing commands = - pytest -n1 -ra {posargs:testing} + pytest -n1 -ra {posargs:.} [testenv:py36-xdist] deps = {[testenv:py27-xdist]deps} @@ -81,10 +82,11 @@ deps = pytest-xdist>=1.13 hypothesis>=3.5.2 distribute = true +changedir=testing setenv = PYTHONDONTWRITEBYTECODE=1 commands = - pytest -n3 -ra {posargs:testing} + pytest -n3 -ra {posargs:.} [testenv:py27-trial] deps = twisted From 460cae02b0d48594730870ae0e6cf8cd9653b865 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Nov 2017 16:51:59 -0200 Subject: [PATCH 126/159] Small formatting fix in CHANGELOG --- changelog/2877.trivial | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2877.trivial b/changelog/2877.trivial index aaf58b039..c4198af97 100644 --- a/changelog/2877.trivial +++ b/changelog/2877.trivial @@ -1 +1 @@ -internal move of the parameterset extraction to a more maintainable place \ No newline at end of file +Internal move of the parameterset extraction to a more maintainable place. From e58e8faf476edebc4cdba54951cdd84efcd10124 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 Nov 2017 17:07:33 -0200 Subject: [PATCH 127/159] Add CHANGELOG entry for attrs module dependency --- changelog/2641.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2641.trivial diff --git a/changelog/2641.trivial b/changelog/2641.trivial new file mode 100644 index 000000000..1799f90d2 --- /dev/null +++ b/changelog/2641.trivial @@ -0,0 +1 @@ +pytest now depends on `attrs `_ for internal structures to ease code maintainability. From c47dcaa7134527abf40bd469864c433dfba1814b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 20 Oct 2017 12:43:17 +0200 Subject: [PATCH 128/159] switch a special case in scope node lookup to a general one --- _pytest/fixtures.py | 4 ++-- changelog/2910.trivial | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog/2910.trivial diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index c5da9f4cf..5dc1aebcc 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -27,10 +27,12 @@ from collections import OrderedDict def pytest_sessionstart(session): import _pytest.python + scopename2class.update({ 'class': _pytest.python.Class, 'module': _pytest.python.Module, 'function': _pytest.main.Item, + 'session': _pytest.main.Session, }) session._fixturemanager = FixtureManager(session) @@ -62,8 +64,6 @@ def scopeproperty(name=None, doc=None): def get_scope_node(node, scope): cls = scopename2class.get(scope) if cls is None: - if scope == "session": - return node.session raise ValueError("unknown scope") return node.getparent(cls) diff --git a/changelog/2910.trivial b/changelog/2910.trivial new file mode 100644 index 000000000..8b8557974 --- /dev/null +++ b/changelog/2910.trivial @@ -0,0 +1 @@ +interal refactoring to simplify scope node lookup \ No newline at end of file From 0108f262b1ad37b609222605182f90a33c3fa067 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 10 Nov 2017 18:15:09 -0200 Subject: [PATCH 129/159] Fix typo in CHANGELOG --- changelog/2910.trivial | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/2910.trivial b/changelog/2910.trivial index 8b8557974..87bf00dfe 100644 --- a/changelog/2910.trivial +++ b/changelog/2910.trivial @@ -1 +1 @@ -interal refactoring to simplify scope node lookup \ No newline at end of file +Internal refactoring to simplify scope node lookup. From 6550b9911bd38e3cb01fab0af388530b46619d4b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 12 Nov 2017 11:14:55 -0200 Subject: [PATCH 130/159] pytest_fixture_post_finalizer now receives a request argument --- _pytest/fixtures.py | 19 +++++------ _pytest/hookspec.py | 2 +- testing/python/fixture.py | 68 +++++++++++++++++---------------------- 3 files changed, 41 insertions(+), 48 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 7b50b8574..5e3474ac3 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -4,6 +4,7 @@ import inspect import sys import warnings +import functools import py from py._code.code import FormattedExcinfo @@ -521,7 +522,7 @@ class FixtureRequest(FuncargnamesCompatAttr): val = fixturedef.execute(request=subrequest) finally: # if fixture function failed it might have registered finalizers - self.session._setupstate.addfinalizer(fixturedef.finish, + self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest), subrequest.node) return val @@ -744,7 +745,7 @@ class FixtureDef: def addfinalizer(self, finalizer): self._finalizer.append(finalizer) - def finish(self): + def finish(self, request): exceptions = [] try: while self._finalizer: @@ -759,10 +760,12 @@ class FixtureDef: py.builtin._reraise(*e) finally: - hook = self._fixturemanager.session.config.hook - hook.pytest_fixture_post_finalizer(fixturedef=self) + 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 + # 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 @@ -772,7 +775,7 @@ class FixtureDef: for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) if argname != "request": - fixturedef.addfinalizer(self.finish) + fixturedef.addfinalizer(functools.partial(self.finish, request=request)) my_cache_key = request.param_index cached_result = getattr(self, "cached_result", None) @@ -788,9 +791,7 @@ class FixtureDef: self.finish() assert not hasattr(self, "cached_result") - hook = self._fixturemanager.session.gethookproxy( - request._pyfuncitem.fspath - ) + hook = self._fixturemanager.session.gethookproxy(request.node.fspath) return hook.pytest_fixture_setup(fixturedef=self, request=request) def __repr__(self): diff --git a/_pytest/hookspec.py b/_pytest/hookspec.py index e5c966e58..ceb87a3c2 100644 --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -296,7 +296,7 @@ def pytest_fixture_setup(fixturedef, request): Stops at first non-None result, see :ref:`firstresult` """ -def pytest_fixture_post_finalizer(fixturedef): +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.""" diff --git a/testing/python/fixture.py b/testing/python/fixture.py index b351eeeca..f843e1948 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -3127,49 +3127,41 @@ class TestParameterizedSubRequest(object): """.format(fixfile.strpath, testfile.basename)) -def test_pytest_fixture_setup_hook(testdir): +def test_pytest_fixture_setup_and_post_finalizer_hook(testdir): testdir.makeconftest(""" - import pytest - - def pytest_fixture_setup(): - print('pytest_fixture_setup hook called') + from __future__ import print_function + def pytest_fixture_setup(fixturedef, request): + print('ROOT setup hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) + def pytest_fixture_post_finalizer(fixturedef, request): + print('ROOT finalizer hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) """) - testdir.makepyfile(""" - import pytest + testdir.makepyfile(**{ + 'tests/conftest.py': """ + from __future__ import print_function + def pytest_fixture_setup(fixturedef, request): + print('TESTS setup hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) + def pytest_fixture_post_finalizer(fixturedef, request): + print('TESTS finalizer hook called for {0} from {1}'.format(fixturedef.argname, request.node.name)) + """, + 'tests/test_hooks.py': """ + from __future__ import print_function + import pytest - @pytest.fixture() - def some(): - return 'some' + @pytest.fixture() + def my_fixture(): + return 'some' - def test_func(some): - assert some == 'some' - """) + def test_func(my_fixture): + print('TEST test_func') + assert my_fixture == 'some' + """ + }) result = testdir.runpytest("-s") assert result.ret == 0 result.stdout.fnmatch_lines([ - "*pytest_fixture_setup hook called*", - ]) - - -def test_pytest_fixture_post_finalizer_hook(testdir): - testdir.makeconftest(""" - import pytest - - def pytest_fixture_post_finalizer(): - print('pytest_fixture_post_finalizer hook called') - """) - testdir.makepyfile(""" - import pytest - - @pytest.fixture() - def some(): - return 'some' - - def test_func(some): - assert some == 'some' - """) - result = testdir.runpytest("-s") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*pytest_fixture_post_finalizer hook called*", + "*TESTS setup hook called for my_fixture from test_func*", + "*ROOT setup hook called for my_fixture from test_func*", + "*TEST test_func*", + "*TESTS finalizer hook called for my_fixture from test_func*", + "*ROOT finalizer hook called for my_fixture from test_func*", ]) From 063335a715bce042e0c9e591ab8ad406488cb5dc Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 12 Nov 2017 11:19:02 -0200 Subject: [PATCH 131/159] Add changelog entries for #2124 --- changelog/2124.bugfix | 1 + changelog/2124.feature | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/2124.bugfix create mode 100644 changelog/2124.feature diff --git a/changelog/2124.bugfix b/changelog/2124.bugfix new file mode 100644 index 000000000..e1c5e044c --- /dev/null +++ b/changelog/2124.bugfix @@ -0,0 +1 @@ +``pytest_fixture_setup`` and ``pytest_fixture_post_finalizer`` hooks are now called for all ``conftest.py`` files. diff --git a/changelog/2124.feature b/changelog/2124.feature new file mode 100644 index 000000000..267fdabc9 --- /dev/null +++ b/changelog/2124.feature @@ -0,0 +1 @@ +``pytest_fixture_post_finalizer`` hook can now receive a ``request`` argument. From bdad345f991cae76896476a1b574269b23085bc3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 12 Nov 2017 11:28:26 -0200 Subject: [PATCH 132/159] Fix passing request to finish() in FixtureDef --- _pytest/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index f1374b20f..74efd309d 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -785,7 +785,7 @@ class FixtureDef: 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() + self.finish(request) assert not hasattr(self, "cached_result") hook = self._fixturemanager.session.gethookproxy(request.node.fspath) From 6d3fe0b826c2aacf1eec573ed0fd50eb8774ee77 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 12 Nov 2017 11:28:57 -0200 Subject: [PATCH 133/159] Explicitly clear finalizers list in finalize to ensure cleanup --- _pytest/fixtures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 74efd309d..ceae79cb4 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -765,6 +765,7 @@ class FixtureDef: # keep instances alive if hasattr(self, "cached_result"): del self.cached_result + self._finalizer = [] def execute(self, request): # get required arguments and register our own finish() From a6f2d2d2c958d743813414e46041adf3e47fb997 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 12 Nov 2017 11:35:46 -0200 Subject: [PATCH 134/159] Rename FixtureDef.finalizer to FixtureDef.finalizers --- _pytest/fixtures.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index ceae79cb4..19b977224 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -1,14 +1,15 @@ from __future__ import absolute_import, division, print_function +import functools import inspect import sys import warnings +from collections import OrderedDict -import functools +import attr import py from py._code.code import FormattedExcinfo -import attr import _pytest from _pytest import nodes from _pytest._code.code import TerminalRepr @@ -23,9 +24,6 @@ from _pytest.compat import ( from _pytest.outcomes import fail, TEST_OUTCOME -from collections import OrderedDict - - def pytest_sessionstart(session): import _pytest.python @@ -737,17 +735,17 @@ class FixtureDef: self.argnames = getfuncargnames(func, is_method=unittest) self.unittest = unittest self.ids = ids - self._finalizer = [] + self._finalizers = [] def addfinalizer(self, finalizer): - self._finalizer.append(finalizer) + self._finalizers.append(finalizer) def finish(self, request): exceptions = [] try: - while self._finalizer: + while self._finalizers: try: - func = self._finalizer.pop() + func = self._finalizers.pop() func() except: # noqa exceptions.append(sys.exc_info()) @@ -765,7 +763,7 @@ class FixtureDef: # keep instances alive if hasattr(self, "cached_result"): del self.cached_result - self._finalizer = [] + self._finalizers = [] def execute(self, request): # get required arguments and register our own finish() From 8f9081248105eaa704fa79abb5e544571ab8c9e8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 14 Nov 2017 14:08:23 -0800 Subject: [PATCH 135/159] Add capfdbinary fixture `capfdbinary` works like `capfd` but produces bytes for `readouterr()`. --- _pytest/capture.py | 76 ++++++++++++++++++++++++++++++----------- changelog/2923.feature | 2 ++ doc/en/capture.rst | 14 +++++--- testing/test_capture.py | 28 +++++++++++++-- 4 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 changelog/2923.feature diff --git a/_pytest/capture.py b/_pytest/capture.py index f6d3c61b3..fee0116a5 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -180,17 +180,29 @@ class CaptureManager: item.add_report_section(when, "stderr", err) -error_capsysfderror = "cannot use capsys and capfd at the same time" +capture_fixtures = {'capfd', 'capfdbinary', 'capsys'} + + +def _ensure_only_one_capture_fixture(request, name): + fixtures = set(request.fixturenames) & capture_fixtures - set((name,)) + if fixtures: + fixtures = sorted(fixtures) + fixtures = fixtures[0] if len(fixtures) == 1 else fixtures + raise request.raiseerror( + "cannot use {0} and {1} at the same time".format( + fixtures, name, + ), + ) @pytest.fixture def capsys(request): """Enable capturing of writes to sys.stdout/sys.stderr and make captured output available via ``capsys.readouterr()`` method calls - which return a ``(out, err)`` tuple. + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text`` + objects. """ - if "capfd" in request.fixturenames: - raise request.raiseerror(error_capsysfderror) + _ensure_only_one_capture_fixture(request, 'capsys') with _install_capture_fixture_on_item(request, SysCapture) as fixture: yield fixture @@ -199,16 +211,30 @@ def capsys(request): def capfd(request): """Enable capturing of writes to file descriptors 1 and 2 and make captured output available via ``capfd.readouterr()`` method calls - which return a ``(out, err)`` tuple. + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text`` + objects. """ - if "capsys" in request.fixturenames: - request.raiseerror(error_capsysfderror) + _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 capturing of write to file descriptors 1 and 2 and make + captured output available via ``capfdbinary.readouterr`` method calls + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be + ``bytes`` 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): """ @@ -378,8 +404,11 @@ class NoCapture: __init__ = start = done = suspend = resume = lambda *args: None -class FDCapture: - """ Capture IO to/from a given os-level filedescriptor. """ +class FDCaptureBinary: + """Capture IO to/from a given os-level filedescriptor. + + snap() produces `bytes` + """ def __init__(self, targetfd, tmpfile=None): self.targetfd = targetfd @@ -418,17 +447,11 @@ class FDCapture: self.syscapture.start() def snap(self): - f = self.tmpfile - f.seek(0) - res = f.read() - if res: - enc = getattr(f, "encoding", None) - if enc and isinstance(res, bytes): - res = six.text_type(res, enc, "replace") - f.truncate(0) - f.seek(0) - return res - return '' + 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, @@ -454,6 +477,19 @@ class FDCapture: os.write(self.targetfd_save, data) +class FDCapture(FDCaptureBinary): + """Capture IO to/from a given os-level filedescriptor. + + snap() produces text + """ + def snap(self): + res = FDCaptureBinary.snap(self) + enc = getattr(self.tmpfile, "encoding", None) + if enc and isinstance(res, bytes): + res = six.text_type(res, enc, "replace") + return res + + class SysCapture: def __init__(self, fd, tmpfile=None): name = patchsysdict[fd] diff --git a/changelog/2923.feature b/changelog/2923.feature new file mode 100644 index 000000000..c6937741f --- /dev/null +++ b/changelog/2923.feature @@ -0,0 +1,2 @@ +Add ``capfdbinary`` a version of ``capfd`` which returns bytes from +``readouterr()``. diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 58ebdf840..7ec9871dc 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -85,9 +85,9 @@ of the failing function and hide the other one:: Accessing captured output from a test function --------------------------------------------------- -The ``capsys`` and ``capfd`` fixtures allow to access stdout/stderr -output created during test execution. Here is an example test function -that performs some output related checks: +The ``capsys``, ``capfd``, and ``capfdbinary`` fixtures allow access to +stdout/stderr output created during test execution. Here is an example test +function that performs some output related checks: .. code-block:: python @@ -110,11 +110,17 @@ output streams and also interacts well with pytest's own per-test capturing. If you want to capture on filedescriptor level you can use -the ``capfd`` function argument which offers the exact +the ``capfd`` fixture which offers the exact same interface but allows to also capture output from libraries or subprocesses that directly write to operating system level output streams (FD1 and FD2). +.. versionadded:: 3.3 + +If the code under test writes non-textual data, you can capture this using +the ``capfdbinary`` fixture which instead returns ``bytes`` from +the ``readouterr`` method. + .. versionadded:: 3.0 diff --git a/testing/test_capture.py b/testing/test_capture.py index a21e767a8..4aad385b3 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -398,7 +398,7 @@ class TestCaptureFixture(object): result = testdir.runpytest(p) result.stdout.fnmatch_lines([ "*ERROR*setup*test_one*", - "E*capsys*capfd*same*time*", + "E*capfd*capsys*same*time*", "*ERROR*setup*test_two*", "E*capsys*capfd*same*time*", "*2 error*"]) @@ -418,10 +418,21 @@ class TestCaptureFixture(object): "*test_one*", "*capsys*capfd*same*time*", "*test_two*", - "*capsys*capfd*same*time*", + "*capfd*capsys*same*time*", "*2 failed in*", ]) + def test_capsyscapfdbinary(self, testdir): + p = testdir.makepyfile(""" + def test_one(capsys, capfdbinary): + pass + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ERROR*setup*test_one*", + "E*capfdbinary*capsys*same*time*", + "*1 error*"]) + @pytest.mark.parametrize("method", ["sys", "fd"]) def test_capture_is_represented_on_failure_issue128(self, testdir, method): p = testdir.makepyfile(""" @@ -446,6 +457,19 @@ class TestCaptureFixture(object): """) reprec.assertoutcome(passed=1) + @needsosdup + def test_capfdbinary(self, testdir): + reprec = testdir.inline_runsource(""" + def test_hello(capfdbinary): + import os + # some likely un-decodable bytes + os.write(1, b'\\xfe\\x98\\x20') + out, err = capfdbinary.readouterr() + assert out == b'\\xfe\\x98\\x20' + assert err == b'' + """) + reprec.assertoutcome(passed=1) + def test_partial_setup_failure(self, testdir): p = testdir.makepyfile(""" def test_hello(capsys, missingarg): From e87ff07370cab38fbd22fdadf89c82c0d92a1e5f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 14 Nov 2017 22:27:15 -0200 Subject: [PATCH 136/159] Prevent pip from installing pytest in unsupported Python versions Fix #2922 --- changelog/2922.trivial | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/2922.trivial diff --git a/changelog/2922.trivial b/changelog/2922.trivial new file mode 100644 index 000000000..06d1346bb --- /dev/null +++ b/changelog/2922.trivial @@ -0,0 +1 @@ +Configure ``pytest`` to prevent pip from installing pytest in unsupported Python versions. diff --git a/setup.py b/setup.py index c4fc48920..54ec9581a 100644 --- a/setup.py +++ b/setup.py @@ -83,6 +83,7 @@ def main(): cmdclass={'test': PyTest}, # the following should be enabled for release setup_requires=['setuptools-scm'], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', install_requires=install_requires, extras_require=extras_require, packages=['_pytest', '_pytest.assertion', '_pytest._code'], From c7be83ac47363be6db6dc28e79b7b1d82ad319d9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Nov 2017 13:00:29 -0200 Subject: [PATCH 137/159] Update pluggy pin to 0.5.* --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d8ed259de..d684072ab 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ def main(): # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ: - install_requires.append('pluggy>=0.4.0,<0.5') + install_requires.append('pluggy>=0.5,<0.6') if has_environment_marker_support(): extras_require[':python_version<"3.0"'] = ['funcsigs'] extras_require[':sys_platform=="win32"'] = ['colorama'] From e2c59d3282044e3c90e6ec9a96a61aa64f22a75d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Nov 2017 14:08:11 -0200 Subject: [PATCH 138/159] Change pluggy._CallOutcome to pluggy._Result after update to pluggy-0.5 --- doc/en/writing_plugins.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 53a14cb0d..c2599ffbd 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -452,7 +452,7 @@ hook wrappers and passes the same arguments as to the regular hooks. At the yield point of the hook wrapper pytest will execute the next hook implementations and return their result to the yield point in the form of -a :py:class:`CallOutcome ` instance which encapsulates a result or +a :py:class:`Result ` instance which encapsulates a result or exception info. The yield point itself will thus typically not raise exceptions (unless there are bugs). @@ -517,7 +517,7 @@ Here is the order of execution: Plugin1). 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield - point. The yield receives a :py:class:`CallOutcome ` instance which encapsulates + point. The yield receives a :py:class:`Result ` instance which encapsulates the result from calling the non-wrappers. Wrappers shall not modify the result. It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with @@ -714,7 +714,7 @@ Reference of objects involved in hooks :members: :inherited-members: -.. autoclass:: pluggy._CallOutcome() +.. autoclass:: pluggy._Result :members: .. autofunction:: _pytest.config.get_plugin_manager() From 1f08d990d53ad3e22f8688086d0b3bd5554430a9 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Nov 2017 13:48:08 -0200 Subject: [PATCH 139/159] Deprecate metafunc.addcall Fix #2876 --- _pytest/deprecated.py | 5 +++++ _pytest/python.py | 12 +++++++++--- testing/deprecated_test.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/_pytest/deprecated.py b/_pytest/deprecated.py index 910510b01..9c0fbeca7 100644 --- a/_pytest/deprecated.py +++ b/_pytest/deprecated.py @@ -45,3 +45,8 @@ COLLECTOR_MAKEITEM = RemovedInPytest4Warning( "pycollector makeitem was removed " "as it is an accidentially leaked internal api" ) + +METAFUNC_ADD_CALL = ( + "Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n" + "Please use Metafunc.parametrize instead." +) diff --git a/_pytest/python.py b/_pytest/python.py index 749024b01..650171a9e 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -837,9 +837,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): self._calls = newcalls def addcall(self, funcargs=None, id=NOTSET, param=NOTSET): - """ (deprecated, use parametrize) Add a new call to the underlying - test function during the collection phase of a test run. Note that - request.addcall() is called during the test collection phase prior and + """ Add a new call to the underlying test function during the collection phase of a test run. + + .. deprecated:: 3.3 + + Use :meth:`parametrize` instead. + + Note that request.addcall() is called during the test collection phase prior and independently to actual test execution. You should only use addcall() if you need to specify multiple arguments of a test function. @@ -852,6 +856,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): :arg param: a parameter which will be exposed to a later fixture function invocation through the ``request.param`` attribute. """ + if self.config: + self.config.warn('C1', message=deprecated.METAFUNC_ADD_CALL, fslocation=None) assert funcargs is None or isinstance(funcargs, dict) if funcargs is not None: for name in funcargs: diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 3f244a53c..6e1e16331 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -82,3 +82,20 @@ def test_resultlog_is_deprecated(testdir): '*--result-log is deprecated and scheduled for removal in pytest 4.0*', '*See https://docs.pytest.org/*/usage.html#creating-resultlog-format-files for more information*', ]) + + +@pytest.mark.filterwarnings('always:Metafunc.addcall is deprecated') +def test_metafunc_addcall_deprecated(testdir): + testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall({'i': 1}) + metafunc.addcall({'i': 2}) + def test_func(i): + pass + """) + res = testdir.runpytest('-s') + assert res.ret == 0 + res.stdout.fnmatch_lines([ + "*Metafunc.addcall is deprecated*", + "*2 passed, 2 warnings*", + ]) From 99ba3c9700239787640a310ef975915219ddc85e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 15 Nov 2017 22:02:47 -0200 Subject: [PATCH 140/159] Add CHANGELOG entry for #2876 --- changelog/2876.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2876.trivial diff --git a/changelog/2876.trivial b/changelog/2876.trivial new file mode 100644 index 000000000..354f6ad5c --- /dev/null +++ b/changelog/2876.trivial @@ -0,0 +1 @@ +Calls to ``Metafunc.addcall`` now emit a deprecation warning. This function is scheduled to be removed in ``pytest-4.0``. From 219b758949a473507d46f0105b1853319c971b6c Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Nov 2017 09:02:46 -0800 Subject: [PATCH 141/159] Add capsysbinary fixture `capsysbinary` works like `capsys` but produces bytes for `readouterr()`. --- _pytest/capture.py | 33 ++++++++++++++++++++++++++++----- changelog/2934.feature | 2 ++ doc/en/capture.rst | 17 +++++++++++++---- testing/test_capture.py | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 changelog/2934.feature diff --git a/_pytest/capture.py b/_pytest/capture.py index fee0116a5..f2ebe38c8 100644 --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -180,7 +180,7 @@ class CaptureManager: item.add_report_section(when, "stderr", err) -capture_fixtures = {'capfd', 'capfdbinary', 'capsys'} +capture_fixtures = {'capfd', 'capfdbinary', 'capsys', 'capsysbinary'} def _ensure_only_one_capture_fixture(request, name): @@ -207,6 +207,22 @@ def capsys(request): yield fixture +@pytest.fixture +def capsysbinary(request): + """Enable capturing of writes to sys.stdout/sys.stderr and make + captured output available via ``capsys.readouterr()`` method calls + which return a ``(out, err)`` tuple. ``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,): + 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): """Enable capturing of writes to file descriptors 1 and 2 and make @@ -506,10 +522,9 @@ class SysCapture: setattr(sys, self.name, self.tmpfile) def snap(self): - f = self.tmpfile - res = f.getvalue() - f.truncate(0) - f.seek(0) + res = self.tmpfile.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() return res def done(self): @@ -528,6 +543,14 @@ class SysCapture: self._old.flush() +class SysCaptureBinary(SysCapture): + def snap(self): + res = self.tmpfile.buffer.getvalue() + self.tmpfile.seek(0) + self.tmpfile.truncate() + return res + + class DontReadFromInput: """Temporary stub class. Ideally when stdin is accessed, the capturing should be turned off, with possibly all data captured diff --git a/changelog/2934.feature b/changelog/2934.feature new file mode 100644 index 000000000..38dcb5f73 --- /dev/null +++ b/changelog/2934.feature @@ -0,0 +1,2 @@ +Add ``capsysbinary`` a version of ``capsys`` which returns bytes from +``readouterr()``. diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 7ec9871dc..255719961 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -85,9 +85,9 @@ of the failing function and hide the other one:: Accessing captured output from a test function --------------------------------------------------- -The ``capsys``, ``capfd``, and ``capfdbinary`` fixtures allow access to -stdout/stderr output created during test execution. Here is an example test -function that performs some output related checks: +The ``capsys``, ``capsysbinary``, ``capfd``, and ``capfdbinary`` fixtures +allow access to stdout/stderr output created during test execution. Here is +an example test function that performs some output related checks: .. code-block:: python @@ -115,11 +115,20 @@ same interface but allows to also capture output from libraries or subprocesses that directly write to operating system level output streams (FD1 and FD2). +.. versionadded:: 3.3 + +If the code under test writes non-textual data, you can capture this using +the ``capsysbinary`` fixture which instead returns ``bytes`` from +the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only +available in python 3. + + .. versionadded:: 3.3 If the code under test writes non-textual data, you can capture this using the ``capfdbinary`` fixture which instead returns ``bytes`` from -the ``readouterr`` method. +the ``readouterr`` method. The ``capfdbinary`` fixture operates on the +filedescriptor level. .. versionadded:: 3.0 diff --git a/testing/test_capture.py b/testing/test_capture.py index 4aad385b3..3e8162f71 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -470,6 +470,38 @@ class TestCaptureFixture(object): """) reprec.assertoutcome(passed=1) + @pytest.mark.skipif( + sys.version_info < (3,), + reason='only have capsysbinary in python 3', + ) + def test_capsysbinary(self, testdir): + reprec = testdir.inline_runsource(""" + def test_hello(capsysbinary): + import sys + # some likely un-decodable bytes + sys.stdout.buffer.write(b'\\xfe\\x98\\x20') + out, err = capsysbinary.readouterr() + assert out == b'\\xfe\\x98\\x20' + assert err == b'' + """) + reprec.assertoutcome(passed=1) + + @pytest.mark.skipif( + sys.version_info >= (3,), + reason='only have capsysbinary in python 3', + ) + def test_capsysbinary_forbidden_in_python2(self, testdir): + testdir.makepyfile(""" + def test_hello(capsysbinary): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*test_hello*", + "*capsysbinary is only supported on python 3*", + "*1 error in*", + ]) + def test_partial_setup_failure(self, testdir): p = testdir.makepyfile(""" def test_hello(capsys, missingarg): @@ -1233,7 +1265,7 @@ def test_dontreadfrominput_has_encoding(testdir): reprec.assertoutcome(passed=1) -def test_pickling_and_unpickling_enocded_file(): +def test_pickling_and_unpickling_encoded_file(): # See https://bitbucket.org/pytest-dev/pytest/pull-request/194 # pickle.loads() raises infinite recursion if # EncodedFile.__getattr__ is not implemented properly From 7a7cb8c8c501c869994a3bcac542aab4f39af0b4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 14 Nov 2017 20:22:25 -0200 Subject: [PATCH 142/159] Require py>=1.5.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d684072ab..f586101ea 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def has_environment_marker_support(): def main(): extras_require = {} install_requires = [ - 'py>=1.4.33,<1.5', + 'py>=1.5.0', 'six>=1.10.0', 'setuptools', 'attrs>=17.2.0', From dab889304e78a734b41baa8b88146a8fef83a146 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 27 Sep 2017 14:42:55 -0300 Subject: [PATCH 143/159] Implement progress percentage reporting while running tests Fix #2657 --- _pytest/terminal.py | 56 +++++++++++++++++++++++++++++++----- testing/acceptance_test.py | 10 +++---- testing/python/fixture.py | 8 ++++++ testing/python/metafunc.py | 14 +++++---- testing/python/setup_only.py | 2 +- testing/test_assertion.py | 4 +-- testing/test_capture.py | 2 +- testing/test_terminal.py | 12 ++++---- 8 files changed, 81 insertions(+), 27 deletions(-) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 2b13fe1ea..eb7b03e35 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -51,6 +51,10 @@ def pytest_addoption(parser): 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.", + default='progress') + def pytest_configure(config): config.option.verbose -= config.option.quiet @@ -135,16 +139,20 @@ class TerminalReporter: self.showfspath = self.verbosity >= 0 self.showlongtestinfo = self.verbosity > 0 self._numcollected = 0 + self._session = None self.stats = {} self.startdir = py.path.local() if file is None: file = sys.stdout self._writer = _pytest.config.create_terminal_writer(config, file) + self._screen_width = self.writer.fullwidth self.currentfspath = None self.reportchars = getreportopt(config) self.hasmarkup = self.writer.hasmarkup self.isatty = file.isatty() + self._progress_items_reported = 0 + self._show_progress_info = self.config.getini('console_output_style') == 'progress' @property def writer(self): @@ -163,6 +171,8 @@ class TerminalReporter: def write_fspath_result(self, nodeid, res): fspath = self.config.rootdir.join(nodeid.split("::")[0]) if fspath != self.currentfspath: + if self.currentfspath is not None: + self._write_progress_information_filling_space() self.currentfspath = fspath fspath = self.startdir.bestrelpath(fspath) self.writer.line() @@ -177,6 +187,7 @@ class TerminalReporter: if extra: self.writer.write(extra, **kwargs) self.currentfspath = -2 + self._write_progress_information_filling_space() def ensure_newline(self): if self.currentfspath: @@ -256,20 +267,25 @@ class TerminalReporter: rep = report res = self.config.hook.pytest_report_teststatus(report=rep) cat, letter, word = res + if isinstance(word, tuple): + word, markup = word + else: + markup = None self.stats.setdefault(cat, []).append(rep) self._tests_ran = True if not letter and not word: # probably passed setup/teardown return + running_xdist = hasattr(rep, 'node') + self._progress_items_reported += 1 if self.verbosity <= 0: - if not hasattr(rep, 'node') and self.showfspath: + if not running_xdist and self.showfspath: self.write_fspath_result(rep.nodeid, letter) else: self.writer.write(letter) + self._write_progress_if_past_edge() else: - if isinstance(word, tuple): - word, markup = word - else: + if markup is None: if rep.passed: markup = {'green': True} elif rep.failed: @@ -279,17 +295,42 @@ class TerminalReporter: else: markup = {} line = self._locationline(rep.nodeid, *rep.location) - if not hasattr(rep, 'node'): + if not running_xdist: self.write_ensure_prefix(line, word, **markup) - # self.writer.write(word, **markup) else: self.ensure_newline() - if hasattr(rep, 'node'): + if running_xdist: self.writer.write("[%s] " % rep.node.gateway.id) self.writer.write(word, **markup) self.writer.write(" " + line) self.currentfspath = -2 + def _write_progress_if_past_edge(self): + if not self._show_progress_info: + return + last_item = self._progress_items_reported == self._session.testscollected + if last_item: + self._write_progress_information_filling_space() + return + + past_edge = self.writer.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width + if past_edge: + msg = self._get_progress_information_message() + self.writer.write(msg + '\n', cyan=True) + + _PROGRESS_LENGTH = len(' [100%]') + + def _get_progress_information_message(self): + progress = self._progress_items_reported * 100 // self._session.testscollected + return ' [{:3d}%]'.format(progress) + + def _write_progress_information_filling_space(self): + if not self._show_progress_info: + return + msg = self._get_progress_information_message() + fill = ' ' * (self.writer.fullwidth - self.writer.chars_on_current_line - len(msg) - 1) + self.write(fill + msg, cyan=True) + def pytest_collection(self): if not self.isatty and self.config.option.verbose >= 1: self.write("collecting ... ", bold=True) @@ -332,6 +373,7 @@ class TerminalReporter: @pytest.hookimpl(trylast=True) def pytest_sessionstart(self, session): + self._session = session self._sessionstarttime = time.time() if not self.showheader: return diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 111be956b..48eb60a3f 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -630,10 +630,10 @@ class TestInvocationVariants(object): testdir.chdir() assert result.ret == 0 result.stdout.fnmatch_lines([ - "*test_hello.py::test_hello*PASSED", - "*test_hello.py::test_other*PASSED", - "*test_world.py::test_world*PASSED", - "*test_world.py::test_other*PASSED", + "*test_hello.py::test_hello*PASSED*", + "*test_hello.py::test_other*PASSED*", + "*test_world.py::test_world*PASSED*", + "*test_world.py::test_other*PASSED*", "*4 passed*" ]) @@ -641,7 +641,7 @@ class TestInvocationVariants(object): result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other") assert result.ret == 0 result.stdout.fnmatch_lines([ - "*test_world.py::test_other*PASSED", + "*test_world.py::test_other*PASSED*", "*1 passed*" ]) diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 1a0603a80..b159e8ebb 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -2119,6 +2119,10 @@ class TestFixtureMarker(object): assert values == [1, 1, 2, 2] def test_module_parametrized_ordering(self, testdir): + testdir.makeini(""" + [pytest] + console_output_style=classic + """) testdir.makeconftest(""" import pytest @@ -2165,6 +2169,10 @@ class TestFixtureMarker(object): """) def test_class_ordering(self, testdir): + testdir.makeini(""" + [pytest] + console_output_style=classic + """) testdir.makeconftest(""" import pytest diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index a8b6d685b..2ffb7bb5d 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -960,6 +960,10 @@ class TestMetafuncFunctional(object): ]) def test_parametrize_with_ids(self, testdir): + testdir.makeini(""" + [pytest] + console_output_style=classic + """) testdir.makepyfile(""" import pytest def pytest_generate_tests(metafunc): @@ -1005,9 +1009,9 @@ class TestMetafuncFunctional(object): result = testdir.runpytest("-v") assert result.ret == 1 result.stdout.fnmatch_lines_random([ - "*test_function*basic*PASSED", - "*test_function*1-1*PASSED", - "*test_function*advanced*FAILED", + "*test_function*basic*PASSED*", + "*test_function*1-1*PASSED*", + "*test_function*advanced*FAILED*", ]) def test_fixture_parametrized_empty_ids(self, testdir): @@ -1062,8 +1066,8 @@ class TestMetafuncFunctional(object): result = testdir.runpytest("-v") assert result.ret == 1 result.stdout.fnmatch_lines_random([ - "*test_function*a0*PASSED", - "*test_function*a1*FAILED" + "*test_function*a0*PASSED*", + "*test_function*a1*FAILED*" ]) @pytest.mark.parametrize(("scope", "length"), diff --git a/testing/python/setup_only.py b/testing/python/setup_only.py index 18af56477..ab34312fc 100644 --- a/testing/python/setup_only.py +++ b/testing/python/setup_only.py @@ -238,6 +238,6 @@ def test_show_fixtures_and_execute_test(testdir): result.stdout.fnmatch_lines([ '*SETUP F arg*', - '*test_arg (fixtures used: arg)F', + '*test_arg (fixtures used: arg)F*', '*TEARDOWN F arg*', ]) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 4cd050d8c..6c1443c30 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -820,7 +820,7 @@ def test_traceback_failure(testdir): """) result = testdir.runpytest(p1, "--tb=long") result.stdout.fnmatch_lines([ - "*test_traceback_failure.py F", + "*test_traceback_failure.py F*", "====* FAILURES *====", "____*____", "", @@ -840,7 +840,7 @@ def test_traceback_failure(testdir): result = testdir.runpytest(p1) # "auto" result.stdout.fnmatch_lines([ - "*test_traceback_failure.py F", + "*test_traceback_failure.py F*", "====* FAILURES *====", "____*____", "", diff --git a/testing/test_capture.py b/testing/test_capture.py index 3e8162f71..f769a725d 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -266,7 +266,7 @@ class TestPerTestCapturing(object): """) result = testdir.runpytest(p1) result.stdout.fnmatch_lines([ - "*test_capturing_outerr.py .F", + "*test_capturing_outerr.py .F*", "====* FAILURES *====", "____*____", "*test_capturing_outerr.py:8: ValueError", diff --git a/testing/test_terminal.py b/testing/test_terminal.py index b7c880feb..122aba862 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -78,7 +78,7 @@ class TestTerminal(object): ]) else: result.stdout.fnmatch_lines([ - "*test_pass_skip_fail.py .sF" + "*test_pass_skip_fail.py .sF*" ]) result.stdout.fnmatch_lines([ " def test_func():", @@ -142,12 +142,12 @@ class TestTerminal(object): """) result = testdir.runpytest(p2) result.stdout.fnmatch_lines([ - "*test_p2.py .", + "*test_p2.py .*", "*1 passed*", ]) result = testdir.runpytest("-v", p2) result.stdout.fnmatch_lines([ - "*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED", + "*test_p2.py::TestMore::test_p1* <- *test_p1.py*PASSED*", ]) def test_itemreport_directclasses_not_shown_as_subclasses(self, testdir): @@ -431,7 +431,7 @@ class TestTerminalFunctional(object): ) result = testdir.runpytest("-k", "test_two:", testpath) result.stdout.fnmatch_lines([ - "*test_deselected.py ..", + "*test_deselected.py ..*", "=* 1 test*deselected *=", ]) assert result.ret == 0 @@ -464,7 +464,7 @@ class TestTerminalFunctional(object): finally: old.chdir() result.stdout.fnmatch_lines([ - "test_passes.py ..", + "test_passes.py ..*", "* 2 pass*", ]) assert result.ret == 0 @@ -481,7 +481,7 @@ class TestTerminalFunctional(object): "platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" % ( py.std.sys.platform, verinfo, pytest.__version__, py.__version__, pluggy.__version__), - "*test_header_trailer_info.py .", + "*test_header_trailer_info.py .*", "=* 1 passed*in *.[0-9][0-9] seconds *=", ]) if pytest.config.pluginmanager.list_plugin_distinfo(): From 8b92527d7dbd33ee316dfe1abfba7d7c3dfe0517 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Nov 2017 20:05:11 -0200 Subject: [PATCH 144/159] Add re_match_lines and re_match_lines_random to pytester fixture --- _pytest/pytester.py | 51 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index 539b7fdfa..abfd2c078 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1077,6 +1077,23 @@ class LineMatcher: return lines2 def fnmatch_lines_random(self, lines2): + """Check lines exist in the output using ``fnmatch.fnmatch``, 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, fnmatch) + + def re_match_lines_random(self, lines2): + """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. + + """ + 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 @@ -1086,7 +1103,7 @@ class LineMatcher: lines2 = self._getlines(lines2) for line in lines2: for x in self.lines: - if line == x or fnmatch(x, line): + if line == x or match_func(x, line): self._log("matched: ", repr(line)) break else: @@ -1111,13 +1128,37 @@ class LineMatcher: return '\n'.join(self._log_output) def fnmatch_lines(self, lines2): - """Search the text for matching lines. + """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 an pytest.fail() is + use glob wildcards. If they do not match a pytest.fail() is called. The matches and non-matches are also printed on stdout. + """ + 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. + """ + 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. """ lines2 = self._getlines(lines2) lines1 = self.lines[:] @@ -1131,8 +1172,8 @@ class LineMatcher: if line == nextline: self._log("exact match:", repr(line)) break - elif fnmatch(nextline, line): - self._log("fnmatch:", repr(line)) + elif match_func(nextline, line): + self._log("%s:" % match_nickname, repr(line)) self._log(" with:", repr(nextline)) break else: From 3441084bd29f044d26637070c555e59f8fedf0b5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Nov 2017 20:42:15 -0200 Subject: [PATCH 145/159] Add progress output for verbose mode with xdist --- _pytest/terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index eb7b03e35..930a0c507 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -299,8 +299,8 @@ class TerminalReporter: self.write_ensure_prefix(line, word, **markup) else: self.ensure_newline() - if running_xdist: - self.writer.write("[%s] " % rep.node.gateway.id) + self.writer.write("[%s]" % rep.node.gateway.id) + self.writer.write(self._get_progress_information_message() + " ", cyan=True) self.writer.write(word, **markup) self.writer.write(" " + line) self.currentfspath = -2 From 3a5dbabf6047859e2a885cbb2235e948af99aeac Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Nov 2017 20:43:12 -0200 Subject: [PATCH 146/159] Add tests for progress output and docs for ``console_output_style`` Fix #2657 --- _pytest/terminal.py | 7 +++-- doc/en/customize.rst | 19 ++++++++++++++ testing/test_terminal.py | 55 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 930a0c507..92bf172cb 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -52,7 +52,7 @@ def pytest_addoption(parser): help="color terminal output (yes/no/auto).") parser.addini("console_output_style", - help="console output: classic or with additional progress information.", + help="console output: classic or with additional progress information (classic|progress).", default='progress') @@ -300,7 +300,10 @@ class TerminalReporter: else: self.ensure_newline() self.writer.write("[%s]" % rep.node.gateway.id) - self.writer.write(self._get_progress_information_message() + " ", cyan=True) + if self._show_progress_info: + self.writer.write(self._get_progress_information_message() + " ", cyan=True) + else: + self.writer.write(' ') self.writer.write(word, **markup) self.writer.write(" " + line) self.currentfspath = -2 diff --git a/doc/en/customize.rst b/doc/en/customize.rst index 21deb582e..8133704a5 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -312,3 +312,22 @@ Builtin configuration file options relative to :ref:`rootdir `. Additionally path may contain environment variables, that will be expanded. For more information about cache plugin please refer to :ref:`cache_provider`. + + +.. confval:: console_output_style + + .. versionadded:: 3.3 + + Sets the console output style while running tests: + + * ``classic``: classic pytest output. + * ``progress``: like classic pytest output, but with a progress indicator. + + The default is ``progress``, but you can fallback to ``classic`` if you prefer or + the new mode is causing unexpected problems: + + .. code-block:: ini + + # content of pytest.ini + [pytest] + console_output_style = classic diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 122aba862..1ee506688 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -964,3 +964,58 @@ def test_no_trailing_whitespace_after_inifile_word(testdir): testdir.makeini('[pytest]') result = testdir.runpytest('') assert 'inifile: tox.ini\n' in result.stdout.str() + + +class TestProgress: + + @pytest.fixture + def many_tests_file(self, testdir): + testdir.makepyfile( + test_bar=""" + import pytest + @pytest.mark.parametrize('i', range(10)) + def test_bar(i): pass + """, + test_foo=""" + import pytest + @pytest.mark.parametrize('i', range(5)) + def test_foo(i): pass + """, + test_foobar=""" + import pytest + @pytest.mark.parametrize('i', range(5)) + def test_foobar(i): pass + """, + ) + + def test_normal(self, many_tests_file, testdir): + output = testdir.runpytest() + output.stdout.re_match_lines([ + r'test_bar.py \.\.\.\.\.\.\.\.\.\. \s+ \[ 50%\]', + r'test_foo.py \.\.\.\.\. \s+ \[ 75%\]', + r'test_foobar.py \.\.\.\.\. \s+ \[100%\]', + ]) + + def test_verbose(self, many_tests_file, testdir): + output = testdir.runpytest('-v') + output.stdout.re_match_lines([ + r'test_bar.py::test_bar\[0\] PASSED \s+ \[ 5%\]', + r'test_foo.py::test_foo\[4\] PASSED \s+ \[ 75%\]', + r'test_foobar.py::test_foobar\[4\] PASSED \s+ \[100%\]', + ]) + + def test_xdist_normal(self, many_tests_file, testdir): + pytest.importorskip('xdist') + output = testdir.runpytest('-n2') + output.stdout.re_match_lines([ + r'\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. \s+ \[100%\]', + ]) + + def test_xdist_verbose(self, many_tests_file, testdir): + pytest.importorskip('xdist') + output = testdir.runpytest('-n2', '-v') + output.stdout.re_match_lines_random([ + r'\[gw\d\] \[\s*\d+%\] PASSED test_bar.py::test_bar\[1\]', + r'\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]', + r'\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]', + ]) From 2455f8670ec22c7e36eb91a97a17af9da6218b77 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Nov 2017 20:57:11 -0200 Subject: [PATCH 147/159] Add changelog --- changelog/2657.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/2657.feature diff --git a/changelog/2657.feature b/changelog/2657.feature new file mode 100644 index 000000000..d0cc3bfd4 --- /dev/null +++ b/changelog/2657.feature @@ -0,0 +1 @@ +Now pytest displays the total progress percentage while running tests. The previous output style can be set by setting the new ``console_output_style`` to ``classic``. From f05333ab75dbd0b4a519616bbcdf762c9f3d07e6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Nov 2017 22:04:30 -0200 Subject: [PATCH 148/159] Fix rewrite to not write past the edge of the screen --- _pytest/terminal.py | 2 +- testing/test_terminal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 92bf172cb..7a18b94a7 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -214,7 +214,7 @@ class TerminalReporter: """ erase = markup.pop('erase', False) if erase: - fill_count = self.writer.fullwidth - len(line) + fill_count = self.writer.fullwidth - len(line) - 1 fill = ' ' * fill_count else: fill = '' diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1ee506688..fd68ae637 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -222,7 +222,7 @@ class TestTerminal(object): tr.writer.fullwidth = 10 tr.write('hello') tr.rewrite('hey', erase=True) - assert f.getvalue() == 'hello' + '\r' + 'hey' + (7 * ' ') + assert f.getvalue() == 'hello' + '\r' + 'hey' + (6 * ' ') class TestCollectonly(object): From 1d26f3730f6223b1834d0e3e07bf10e6c5897046 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 22 Nov 2017 17:15:59 -0200 Subject: [PATCH 149/159] Fix docstrings in pytester --- _pytest/pytester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/pytester.py b/_pytest/pytester.py index abfd2c078..835fed385 100644 --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -1085,10 +1085,10 @@ class LineMatcher: self._match_lines_random(lines2, fnmatch) def re_match_lines_random(self, lines2): - """Check lines exist in the output. + """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. Each line can contain glob whildcards. + output, in any order. """ self._match_lines_random(lines2, lambda name, pat: re.match(pat, name)) From dc574c60ef186969a72d56375eb204fae820312b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 22 Nov 2017 17:16:25 -0200 Subject: [PATCH 150/159] Use regex match-count syntax to improve test readability --- testing/test_terminal.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index fd68ae637..485a3d8c4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -991,9 +991,9 @@ class TestProgress: def test_normal(self, many_tests_file, testdir): output = testdir.runpytest() output.stdout.re_match_lines([ - r'test_bar.py \.\.\.\.\.\.\.\.\.\. \s+ \[ 50%\]', - r'test_foo.py \.\.\.\.\. \s+ \[ 75%\]', - r'test_foobar.py \.\.\.\.\. \s+ \[100%\]', + r'test_bar.py \.{10} \s+ \[ 50%\]', + r'test_foo.py \.{5} \s+ \[ 75%\]', + r'test_foobar.py \.{5} \s+ \[100%\]', ]) def test_verbose(self, many_tests_file, testdir): @@ -1008,7 +1008,7 @@ class TestProgress: pytest.importorskip('xdist') output = testdir.runpytest('-n2') output.stdout.re_match_lines([ - r'\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. \s+ \[100%\]', + r'\.{20} \s+ \[100%\]', ]) def test_xdist_verbose(self, many_tests_file, testdir): From e0d236c0317f589811863a0db3c29787ded7375e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Nov 2017 15:33:41 +0000 Subject: [PATCH 151/159] Remove gendoc normalization for '=' and '_' headers Using the default for non-atty terminals (80) so the new progress indicator aligns correctly. --- doc/en/Makefile | 2 - doc/en/assert.rst | 26 +++--- doc/en/builtin.rst | 24 ++++- doc/en/cache.rst | 50 +++++------ doc/en/capture.rst | 10 +-- doc/en/doctest.rst | 6 +- doc/en/example/markers.rst | 132 ++++++++++++++-------------- doc/en/example/nonpython.rst | 26 +++--- doc/en/example/parametrize.rst | 59 +++++++------ doc/en/example/pythoncollection.rst | 12 +-- doc/en/example/reportingdemo.rst | 94 ++++++++++---------- doc/en/example/simple.rst | 122 ++++++++++++------------- doc/en/example/special.rst | 2 +- doc/en/fixture.rst | 80 ++++++++--------- doc/en/getting-started.rst | 44 +++++----- doc/en/index.rst | 18 ++-- doc/en/parametrize.rst | 30 +++---- doc/en/skipping.rst | 8 +- doc/en/tmpdir.rst | 10 +-- doc/en/unittest.rst | 14 +-- doc/en/warnings.rst | 14 +-- 21 files changed, 404 insertions(+), 379 deletions(-) diff --git a/doc/en/Makefile b/doc/en/Makefile index 286bbd8e7..fa8e8266a 100644 --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -13,8 +13,6 @@ PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . REGENDOC_ARGS := \ - --normalize "/={8,} (.*) ={8,}/======= \1 ========/" \ - --normalize "/_{8,} (.*) _{8,}/_______ \1 ________/" \ --normalize "/in \d+.\d+ seconds/in 0.12 seconds/" \ --normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \ --normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \ diff --git a/doc/en/assert.rst b/doc/en/assert.rst index d9e044356..4a852978e 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -25,15 +25,15 @@ to assert that your function returns a certain value. If this assertion fails you will see the return value of the function call:: $ pytest test_assert1.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - test_assert1.py F + test_assert1.py F [100%] - ======= FAILURES ======== - _______ test_function ________ + ================================= FAILURES ================================= + ______________________________ test_function _______________________________ def test_function(): > assert f() == 4 @@ -41,7 +41,7 @@ you will see the return value of the function call:: E + where 3 = f() test_assert1.py:5: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================= 1 failed in 0.12 seconds ========================= ``pytest`` has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary @@ -168,15 +168,15 @@ when it encounters comparisons. For example:: if you run this module:: $ pytest test_assert2.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - test_assert2.py F + test_assert2.py F [100%] - ======= FAILURES ======== - _______ test_set_comparison ________ + ================================= FAILURES ================================= + ___________________________ test_set_comparison ____________________________ def test_set_comparison(): set1 = set("1308") @@ -190,7 +190,7 @@ if you run this module:: E Use -v to get the full diff test_assert2.py:5: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================= 1 failed in 0.12 seconds ========================= Special comparisons are done for a number of cases: @@ -238,9 +238,9 @@ you can run the test module and get the custom output defined in the conftest file:: $ pytest -q test_foocompare.py - F - ======= FAILURES ======== - _______ test_compare ________ + F [100%] + ================================= FAILURES ================================= + _______________________________ test_compare _______________________________ def test_compare(): f1 = Foo(1) diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index b59399a79..d11eb5606 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -91,11 +91,23 @@ You can ask for available builtin or project-custom capsys Enable capturing of writes to sys.stdout/sys.stderr and make captured output available via ``capsys.readouterr()`` method calls - which return a ``(out, err)`` tuple. + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text`` + objects. + capsysbinary + Enable capturing of writes to sys.stdout/sys.stderr and make + captured output available via ``capsys.readouterr()`` method calls + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes`` + objects. capfd Enable capturing of writes to file descriptors 1 and 2 and make captured output available via ``capfd.readouterr()`` method calls - which return a ``(out, err)`` tuple. + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text`` + objects. + capfdbinary + Enable capturing of write to file descriptors 1 and 2 and make + captured output available via ``capfdbinary.readouterr`` method calls + which return a ``(out, err)`` tuple. ``out`` and ``err`` will be + ``bytes`` objects. doctest_namespace Inject names into the doctest namespace. pytestconfig @@ -104,6 +116,14 @@ You can ask for available builtin or project-custom Add extra xml properties to the tag for the calling test. The fixture is callable with ``(name, value)``, with value being automatically xml-encoded. + caplog + Access and control log capturing. + + Captured logs are available through the following 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 monkeypatch The returned ``monkeypatch`` fixture provides these helper methods to modify objects, dictionaries or os.environ:: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index d5d6b653b..c88721b11 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -46,9 +46,9 @@ First, let's create 50 test invocation of which only 2 fail:: If you run this for the first time you will see two failures:: $ pytest -q - .................F.......F........................ - ======= FAILURES ======== - _______ test_num[17] ________ + .................F.......F........................ [100%] + ================================= FAILURES ================================= + _______________________________ test_num[17] _______________________________ i = 17 @@ -59,7 +59,7 @@ If you run this for the first time you will see two failures:: E Failed: bad luck test_50.py:6: Failed - _______ test_num[25] ________ + _______________________________ test_num[25] _______________________________ i = 25 @@ -75,16 +75,16 @@ If you run this for the first time you will see two failures:: If you then run it with ``--lf``:: $ pytest --lf - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 50 items run-last-failure: rerun previous 2 failures - test_50.py FF + test_50.py FF [100%] - ======= FAILURES ======== - _______ test_num[17] ________ + ================================= FAILURES ================================= + _______________________________ test_num[17] _______________________________ i = 17 @@ -95,7 +95,7 @@ If you then run it with ``--lf``:: E Failed: bad luck test_50.py:6: Failed - _______ test_num[25] ________ + _______________________________ test_num[25] _______________________________ i = 25 @@ -106,8 +106,8 @@ If you then run it with ``--lf``:: E Failed: bad luck test_50.py:6: Failed - ======= 48 tests deselected ======== - ======= 2 failed, 48 deselected in 0.12 seconds ======== + =========================== 48 tests deselected ============================ + ================= 2 failed, 48 deselected in 0.12 seconds ================== You have run only the two failing test from the last run, while 48 tests have not been run ("deselected"). @@ -117,16 +117,16 @@ previous failures will be executed first (as can be seen from the series of ``FF`` and dots):: $ pytest --ff - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 50 items run-last-failure: rerun previous 2 failures first - test_50.py FF................................................ + test_50.py FF................................................ [100%] - ======= FAILURES ======== - _______ test_num[17] ________ + ================================= FAILURES ================================= + _______________________________ test_num[17] _______________________________ i = 17 @@ -137,7 +137,7 @@ of ``FF`` and dots):: E Failed: bad luck test_50.py:6: Failed - _______ test_num[25] ________ + _______________________________ test_num[25] _______________________________ i = 25 @@ -148,7 +148,7 @@ of ``FF`` and dots):: E Failed: bad luck test_50.py:6: Failed - ======= 2 failed, 48 passed in 0.12 seconds ======== + =================== 2 failed, 48 passed in 0.12 seconds ==================== .. _`config.cache`: @@ -182,9 +182,9 @@ If you run this command once, it will take a while because of the sleep:: $ pytest -q - F - ======= FAILURES ======== - _______ test_function ________ + F [100%] + ================================= FAILURES ================================= + ______________________________ test_function _______________________________ mydata = 42 @@ -199,9 +199,9 @@ If you run it a second time the value will be retrieved from the cache and this will be quick:: $ pytest -q - F - ======= FAILURES ======== - _______ test_function ________ + F [100%] + ================================= FAILURES ================================= + ______________________________ test_function _______________________________ mydata = 42 @@ -222,7 +222,7 @@ You can always peek at the content of the cache using the ``--cache-show`` command line option:: $ py.test --cache-show - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: cachedir: $REGENDOC_TMPDIR/.cache @@ -232,7 +232,7 @@ You can always peek at the content of the cache using the example/value contains: 42 - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= Clearing Cache content ------------------------------- diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 255719961..a87b57f8f 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -63,15 +63,15 @@ and running this module will show you precisely the output of the failing function and hide the other one:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - test_module.py .F + test_module.py .F [100%] - ======= FAILURES ======== - _______ test_func2 ________ + ================================= FAILURES ================================= + ________________________________ test_func2 ________________________________ def test_func2(): > assert False @@ -80,7 +80,7 @@ of the failing function and hide the other one:: test_module.py:9: AssertionError -------------------------- Captured stdout setup --------------------------- setting up - ======= 1 failed, 1 passed in 0.12 seconds ======== + ==================== 1 failed, 1 passed in 0.12 seconds ==================== Accessing captured output from a test function --------------------------------------------------- diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index f5800fec2..4c5a878dd 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -61,14 +61,14 @@ and another like this:: then you can just invoke ``pytest`` without command line options:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 1 item - mymodule.py . + mymodule.py . [100%] - ======= 1 passed in 0.12 seconds ======== + ========================= 1 passed in 0.12 seconds ========================= It is possible to use fixtures using the ``getfixture`` helper:: diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index e3082f279..43c20d5b7 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -30,32 +30,32 @@ You can "mark" a test function with custom metadata like this:: You can then restrict a test run to only run tests marked with ``webtest``:: $ pytest -v -m webtest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items - test_server.py::test_send_http PASSED + test_server.py::test_send_http PASSED [100%] - ======= 3 tests deselected ======== - ======= 1 passed, 3 deselected in 0.12 seconds ======== + ============================ 3 tests deselected ============================ + ================== 1 passed, 3 deselected in 0.12 seconds ================== Or the inverse, running all tests except the webtest ones:: $ pytest -v -m "not webtest" - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items - test_server.py::test_something_quick PASSED - test_server.py::test_another PASSED - test_server.py::TestClass::test_method PASSED + test_server.py::test_something_quick PASSED [ 33%] + test_server.py::test_another PASSED [ 66%] + test_server.py::TestClass::test_method PASSED [100%] - ======= 1 tests deselected ======== - ======= 3 passed, 1 deselected in 0.12 seconds ======== + ============================ 1 tests deselected ============================ + ================== 3 passed, 1 deselected in 0.12 seconds ================== Selecting tests based on their node ID -------------------------------------- @@ -65,42 +65,42 @@ arguments to select only specified tests. This makes it easy to select tests based on their module, class, method, or function name:: $ pytest -v test_server.py::TestClass::test_method - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item - test_server.py::TestClass::test_method PASSED + test_server.py::TestClass::test_method PASSED [100%] - ======= 1 passed in 0.12 seconds ======== + ========================= 1 passed in 0.12 seconds ========================= You can also select on the class:: $ pytest -v test_server.py::TestClass - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 1 item - test_server.py::TestClass::test_method PASSED + test_server.py::TestClass::test_method PASSED [100%] - ======= 1 passed in 0.12 seconds ======== + ========================= 1 passed in 0.12 seconds ========================= Or select multiple nodes:: $ pytest -v test_server.py::TestClass test_server.py::test_send_http - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items - test_server.py::TestClass::test_method PASSED - test_server.py::test_send_http PASSED + test_server.py::TestClass::test_method PASSED [ 50%] + test_server.py::test_send_http PASSED [100%] - ======= 2 passed in 0.12 seconds ======== + ========================= 2 passed in 0.12 seconds ========================= .. _node-id: @@ -129,47 +129,47 @@ exact match on markers that ``-m`` provides. This makes it easy to select tests based on their names:: $ pytest -v -k http # running with the above defined example module - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items - test_server.py::test_send_http PASSED + test_server.py::test_send_http PASSED [100%] - ======= 3 tests deselected ======== - ======= 1 passed, 3 deselected in 0.12 seconds ======== + ============================ 3 tests deselected ============================ + ================== 1 passed, 3 deselected in 0.12 seconds ================== And you can also run all tests except the ones that match the keyword:: $ pytest -k "not send_http" -v - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items - test_server.py::test_something_quick PASSED - test_server.py::test_another PASSED - test_server.py::TestClass::test_method PASSED + test_server.py::test_something_quick PASSED [ 33%] + test_server.py::test_another PASSED [ 66%] + test_server.py::TestClass::test_method PASSED [100%] - ======= 1 tests deselected ======== - ======= 3 passed, 1 deselected in 0.12 seconds ======== + ============================ 1 tests deselected ============================ + ================== 3 passed, 1 deselected in 0.12 seconds ================== Or to select "http" and "quick" tests:: $ pytest -k "http or quick" -v - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 4 items - test_server.py::test_send_http PASSED - test_server.py::test_something_quick PASSED + test_server.py::test_send_http PASSED [ 50%] + test_server.py::test_something_quick PASSED [100%] - ======= 2 tests deselected ======== - ======= 2 passed, 2 deselected in 0.12 seconds ======== + ============================ 2 tests deselected ============================ + ================== 2 passed, 2 deselected in 0.12 seconds ================== .. note:: @@ -354,26 +354,26 @@ and an example invocations specifying a different environment than what the test needs:: $ pytest -E stage2 - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - test_someenv.py s + test_someenv.py s [100%] - ======= 1 skipped in 0.12 seconds ======== + ======================== 1 skipped in 0.12 seconds ========================= and here is one that specifies exactly the environment needed:: $ pytest -E stage1 - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - test_someenv.py . + test_someenv.py . [100%] - ======= 1 passed in 0.12 seconds ======== + ========================= 1 passed in 0.12 seconds ========================= The ``--markers`` option always gives you a list of available markers:: @@ -432,7 +432,7 @@ The output is as follows:: $ pytest -q -s Marker info name=my_marker args=(,) kwars={} - . + . [100%] 1 passed in 0.12 seconds We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``. @@ -477,7 +477,7 @@ Let's run this without capturing output and see what we get:: glob args=('function',) kwargs={'x': 3} glob args=('class',) kwargs={'x': 2} glob args=('module',) kwargs={'x': 1} - . + . [100%] 1 passed in 0.12 seconds marking platform specific tests with pytest @@ -530,29 +530,29 @@ Let's do a little test file to show how this looks like:: then you will see two tests skipped and two executed tests as expected:: $ pytest -rs # this option reports skip reasons - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items - test_plat.py s.s. - ======= short test summary info ======== + test_plat.py s.s. [100%] + ========================= short test summary info ========================== SKIP [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux - ======= 2 passed, 2 skipped in 0.12 seconds ======== + =================== 2 passed, 2 skipped in 0.12 seconds ==================== Note that if you specify a platform via the marker-command line option like this:: $ pytest -m linux - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items - test_plat.py . + test_plat.py . [100%] - ======= 3 tests deselected ======== - ======= 1 passed, 3 deselected in 0.12 seconds ======== + ============================ 3 tests deselected ============================ + ================== 1 passed, 3 deselected in 0.12 seconds ================== then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. @@ -596,47 +596,47 @@ We want to dynamically define two markers and can do it in a We can now use the ``-m option`` to select one set:: $ pytest -m interface --tb=short - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items - test_module.py FF + test_module.py FF [100%] - ======= FAILURES ======== - _______ test_interface_simple ________ + ================================= FAILURES ================================= + __________________________ test_interface_simple ___________________________ test_module.py:3: in test_interface_simple assert 0 E assert 0 - _______ test_interface_complex ________ + __________________________ test_interface_complex __________________________ test_module.py:6: in test_interface_complex assert 0 E assert 0 - ======= 2 tests deselected ======== - ======= 2 failed, 2 deselected in 0.12 seconds ======== + ============================ 2 tests deselected ============================ + ================== 2 failed, 2 deselected in 0.12 seconds ================== or to select both "event" and "interface" tests:: $ pytest -m "interface or event" --tb=short - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items - test_module.py FFF + test_module.py FFF [100%] - ======= FAILURES ======== - _______ test_interface_simple ________ + ================================= FAILURES ================================= + __________________________ test_interface_simple ___________________________ test_module.py:3: in test_interface_simple assert 0 E assert 0 - _______ test_interface_complex ________ + __________________________ test_interface_complex __________________________ test_module.py:6: in test_interface_complex assert 0 E assert 0 - _______ test_event_simple ________ + ____________________________ test_event_simple _____________________________ test_module.py:9: in test_event_simple assert 0 E assert 0 - ======= 1 tests deselected ======== - ======= 3 failed, 1 deselected in 0.12 seconds ======== + ============================ 1 tests deselected ============================ + ================== 3 failed, 1 deselected in 0.12 seconds ================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 5784f6ed6..cf72c7219 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -26,19 +26,19 @@ and if you installed `PyYAML`_ or a compatible YAML-parser you can now execute the test specification:: nonpython $ pytest test_simple.yml - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collected 2 items - test_simple.yml F. + test_simple.yml F. [100%] - ======= FAILURES ======== - _______ usecase: hello ________ + ================================= FAILURES ================================= + ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ======= 1 failed, 1 passed in 0.12 seconds ======== + ==================== 1 failed, 1 passed in 0.12 seconds ==================== .. regendoc:wipe @@ -58,21 +58,21 @@ your own domain specific testing language this way. consulted when reporting in ``verbose`` mode:: nonpython $ pytest -v - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collecting ... collected 2 items - test_simple.yml::hello FAILED - test_simple.yml::ok PASSED + test_simple.yml::hello FAILED [ 50%] + test_simple.yml::ok PASSED [100%] - ======= FAILURES ======== - _______ usecase: hello ________ + ================================= FAILURES ================================= + ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ======= 1 failed, 1 passed in 0.12 seconds ======== + ==================== 1 failed, 1 passed in 0.12 seconds ==================== .. regendoc:wipe @@ -80,7 +80,7 @@ While developing your custom test collection and execution it's also interesting to just look at the collection tree:: nonpython $ pytest --collect-only - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collected 2 items @@ -88,4 +88,4 @@ interesting to just look at the collection tree:: - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1a8de235a..49852ed7b 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -45,16 +45,16 @@ Now we add a test configuration like this:: This means that we only run 2 tests if we do not pass ``--all``:: $ pytest -q test_compute.py - .. + .. [100%] 2 passed in 0.12 seconds We run only two computations, so we see two dots. let's run the full monty:: $ pytest -q --all - ....F - ======= FAILURES ======== - _______ test_compute[4] ________ + ....F [100%] + ================================= FAILURES ================================= + _____________________________ test_compute[4] ______________________________ param1 = 4 @@ -138,7 +138,7 @@ objects, they are still using the default pytest representation:: $ pytest test_time.py --collect-only - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 8 items @@ -152,7 +152,7 @@ objects, they are still using the default pytest representation:: - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs together with the actual data, instead of listing them separately. @@ -194,20 +194,20 @@ only have to work a bit to construct the correct arguments for pytest's this is a fully self-contained example which you can run with:: $ pytest test_scenarios.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items - test_scenarios.py .... + test_scenarios.py .... [100%] - ======= 4 passed in 0.12 seconds ======== + ========================= 4 passed in 0.12 seconds ========================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function:: $ pytest --collect-only test_scenarios.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items @@ -219,7 +219,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -272,7 +272,7 @@ creates a database object for the actual test invocations:: Let's first see how it looks like at collection time:: $ pytest test_backends.py --collect-only - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items @@ -280,14 +280,14 @@ Let's first see how it looks like at collection time:: - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= And then when we run the test:: $ pytest -q test_backends.py - .F - ======= FAILURES ======== - _______ test_db_initialized[d2] ________ + .F [100%] + ================================= FAILURES ================================= + _________________________ test_db_initialized[d2] __________________________ db = @@ -333,14 +333,14 @@ will be passed to respective fixture function:: The result of this test will be successful:: $ pytest test_indirect_list.py --collect-only - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= .. regendoc:wipe @@ -381,9 +381,9 @@ Our test generator looks up a class-level definition which specifies which argument sets to use for each test function. Let's run it:: $ pytest -q - F.. - ======= FAILURES ======== - _______ TestClass.test_equals[1-2] ________ + F.. [100%] + ================================= FAILURES ================================= + ________________________ TestClass.test_equals[1-2] ________________________ self = , a = 1, b = 2 @@ -411,10 +411,11 @@ is to be run with different sets of arguments for its three arguments: Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize):: . $ pytest -rs -q multipython.py - sssssssssssssss.........sss.........sss......... - ======= short test summary info ======== - SKIP [21] $REGENDOC_TMPDIR/CWD/multipython.py:24: 'python2.6' not found - 27 passed, 21 skipped in 0.12 seconds + ssssssssssssssssssssssss... [100%] + ========================= short test summary info ========================== + SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:24: 'python2.7' not found + SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:24: 'python3.4' not found + 3 passed, 24 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -460,16 +461,16 @@ And finally a little test module:: If you run this with reporting for skips enabled:: $ pytest -rs test_module.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - test_module.py .s - ======= short test summary info ======== + test_module.py .s [100%] + ========================= short test summary info ========================== SKIP [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2' - ======= 1 passed, 1 skipped in 0.12 seconds ======== + =================== 1 passed, 1 skipped in 0.12 seconds ==================== You'll see that we don't have a ``opt2`` module and thus the second test run of our ``test_func1`` was skipped. A few notes: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 18ccae66e..c9d31d7c4 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -116,7 +116,7 @@ that match ``*_check``. For example, if we have:: then the test collection looks like this:: $ pytest --collect-only - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 2 items @@ -126,7 +126,7 @@ then the test collection looks like this:: - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= .. note:: @@ -162,7 +162,7 @@ Finding out what is collected You can always peek at the collection tree without running tests like this:: . $ pytest --collect-only pythoncollection.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 3 items @@ -173,7 +173,7 @@ You can always peek at the collection tree without running tests like this:: - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= .. _customizing-test-collection: @@ -231,9 +231,9 @@ If you run with a Python 3 interpreter both the one test and the ``setup.py`` file will be left out:: $ pytest --collect-only - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2543e840e..9edc02b3c 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -10,15 +10,15 @@ not showing the nice colors here in the HTML that you get on the terminal - we are working on that):: assertion $ pytest failure_demo.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/assertion, inifile: collected 42 items - failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [100%] - ======= FAILURES ======== - _______ test_generative[0] ________ + ================================= FAILURES ================================= + ____________________________ test_generative[0] ____________________________ param1 = 3, param2 = 6 @@ -27,7 +27,7 @@ get on the terminal - we are working on that):: E assert (3 * 2) < 6 failure_demo.py:16: AssertionError - _______ TestFailing.test_simple ________ + _________________________ TestFailing.test_simple __________________________ self = @@ -43,7 +43,7 @@ get on the terminal - we are working on that):: E + and 43 = .g at 0xdeadbeef>() failure_demo.py:29: AssertionError - _______ TestFailing.test_simple_multiline ________ + ____________________ TestFailing.test_simple_multiline _____________________ self = @@ -63,7 +63,7 @@ get on the terminal - we are working on that):: E assert 42 == 54 failure_demo.py:12: AssertionError - _______ TestFailing.test_not ________ + ___________________________ TestFailing.test_not ___________________________ self = @@ -75,7 +75,7 @@ get on the terminal - we are working on that):: E + where 42 = .f at 0xdeadbeef>() failure_demo.py:39: AssertionError - _______ TestSpecialisedExplanations.test_eq_text ________ + _________________ TestSpecialisedExplanations.test_eq_text _________________ self = @@ -86,7 +86,7 @@ get on the terminal - we are working on that):: E + eggs failure_demo.py:43: AssertionError - _______ TestSpecialisedExplanations.test_eq_similar_text ________ + _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ self = @@ -99,7 +99,7 @@ get on the terminal - we are working on that):: E ? ^ failure_demo.py:46: AssertionError - _______ TestSpecialisedExplanations.test_eq_multiline_text ________ + ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ self = @@ -112,7 +112,7 @@ get on the terminal - we are working on that):: E bar failure_demo.py:49: AssertionError - _______ TestSpecialisedExplanations.test_eq_long_text ________ + ______________ TestSpecialisedExplanations.test_eq_long_text _______________ self = @@ -129,7 +129,7 @@ get on the terminal - we are working on that):: E ? ^ failure_demo.py:54: AssertionError - _______ TestSpecialisedExplanations.test_eq_long_text_multiline ________ + _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ self = @@ -149,7 +149,7 @@ get on the terminal - we are working on that):: E ...Full output truncated (7 lines hidden), use '-vv' to show failure_demo.py:59: AssertionError - _______ TestSpecialisedExplanations.test_eq_list ________ + _________________ TestSpecialisedExplanations.test_eq_list _________________ self = @@ -160,7 +160,7 @@ get on the terminal - we are working on that):: E Use -v to get the full diff failure_demo.py:62: AssertionError - _______ TestSpecialisedExplanations.test_eq_list_long ________ + ______________ TestSpecialisedExplanations.test_eq_list_long _______________ self = @@ -173,7 +173,7 @@ get on the terminal - we are working on that):: E Use -v to get the full diff failure_demo.py:67: AssertionError - _______ TestSpecialisedExplanations.test_eq_dict ________ + _________________ TestSpecialisedExplanations.test_eq_dict _________________ self = @@ -191,7 +191,7 @@ get on the terminal - we are working on that):: E ...Full output truncated (2 lines hidden), use '-vv' to show failure_demo.py:70: AssertionError - _______ TestSpecialisedExplanations.test_eq_set ________ + _________________ TestSpecialisedExplanations.test_eq_set __________________ self = @@ -209,7 +209,7 @@ get on the terminal - we are working on that):: E ...Full output truncated (2 lines hidden), use '-vv' to show failure_demo.py:73: AssertionError - _______ TestSpecialisedExplanations.test_eq_longer_list ________ + _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ self = @@ -220,7 +220,7 @@ get on the terminal - we are working on that):: E Use -v to get the full diff failure_demo.py:76: AssertionError - _______ TestSpecialisedExplanations.test_in_list ________ + _________________ TestSpecialisedExplanations.test_in_list _________________ self = @@ -229,7 +229,7 @@ get on the terminal - we are working on that):: E assert 1 in [0, 2, 3, 4, 5] failure_demo.py:79: AssertionError - _______ TestSpecialisedExplanations.test_not_in_text_multiline ________ + __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ self = @@ -248,7 +248,7 @@ get on the terminal - we are working on that):: E ...Full output truncated (2 lines hidden), use '-vv' to show failure_demo.py:83: AssertionError - _______ TestSpecialisedExplanations.test_not_in_text_single ________ + ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ self = @@ -261,7 +261,7 @@ get on the terminal - we are working on that):: E ? +++ failure_demo.py:87: AssertionError - _______ TestSpecialisedExplanations.test_not_in_text_single_long ________ + _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ self = @@ -287,7 +287,7 @@ get on the terminal - we are working on that):: E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ failure_demo.py:95: AssertionError - _______ test_attribute ________ + ______________________________ test_attribute ______________________________ def test_attribute(): class Foo(object): @@ -298,7 +298,7 @@ get on the terminal - we are working on that):: E + where 1 = .Foo object at 0xdeadbeef>.b failure_demo.py:102: AssertionError - _______ test_attribute_instance ________ + _________________________ test_attribute_instance __________________________ def test_attribute_instance(): class Foo(object): @@ -309,7 +309,7 @@ get on the terminal - we are working on that):: E + where .Foo object at 0xdeadbeef> = .Foo'>() failure_demo.py:108: AssertionError - _______ test_attribute_failure ________ + __________________________ test_attribute_failure __________________________ def test_attribute_failure(): class Foo(object): @@ -329,7 +329,7 @@ get on the terminal - we are working on that):: E Exception: Failed to get attrib failure_demo.py:114: Exception - _______ test_attribute_multiple ________ + _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): class Foo(object): @@ -344,7 +344,7 @@ get on the terminal - we are working on that):: E + where .Bar object at 0xdeadbeef> = .Bar'>() failure_demo.py:125: AssertionError - _______ TestRaises.test_raises ________ + __________________________ TestRaises.test_raises __________________________ self = @@ -359,7 +359,7 @@ get on the terminal - we are working on that):: E ValueError: invalid literal for int() with base 10: 'qwe' <0-codegen $PYTHON_PREFIX/lib/python3.5/site-packages/_pytest/python_api.py:580>:1: ValueError - _______ TestRaises.test_raises_doesnt ________ + ______________________ TestRaises.test_raises_doesnt _______________________ self = @@ -368,7 +368,7 @@ get on the terminal - we are working on that):: E Failed: DID NOT RAISE failure_demo.py:137: Failed - _______ TestRaises.test_raise ________ + __________________________ TestRaises.test_raise ___________________________ self = @@ -377,7 +377,7 @@ get on the terminal - we are working on that):: E ValueError: demo error failure_demo.py:140: ValueError - _______ TestRaises.test_tupleerror ________ + ________________________ TestRaises.test_tupleerror ________________________ self = @@ -399,7 +399,7 @@ get on the terminal - we are working on that):: failure_demo.py:148: TypeError --------------------------- Captured stdout call --------------------------- l is [1, 2, 3] - _______ TestRaises.test_some_error ________ + ________________________ TestRaises.test_some_error ________________________ self = @@ -408,7 +408,7 @@ get on the terminal - we are working on that):: E NameError: name 'namenotexi' is not defined failure_demo.py:151: NameError - _______ test_dynamic_compile_shows_nicely ________ + ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): src = 'def foo():\n assert 1 == 0\n' @@ -427,7 +427,7 @@ get on the terminal - we are working on that):: E AssertionError <2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:163>:2: AssertionError - _______ TestMoreErrors.test_complex_error ________ + ____________________ TestMoreErrors.test_complex_error _____________________ self = @@ -451,7 +451,7 @@ get on the terminal - we are working on that):: E assert 44 == 43 failure_demo.py:6: AssertionError - _______ TestMoreErrors.test_z1_unpack_error ________ + ___________________ TestMoreErrors.test_z1_unpack_error ____________________ self = @@ -461,7 +461,7 @@ get on the terminal - we are working on that):: E ValueError: not enough values to unpack (expected 2, got 0) failure_demo.py:180: ValueError - _______ TestMoreErrors.test_z2_type_error ________ + ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -471,7 +471,7 @@ get on the terminal - we are working on that):: E TypeError: 'int' object is not iterable failure_demo.py:184: TypeError - _______ TestMoreErrors.test_startswith ________ + ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -484,7 +484,7 @@ get on the terminal - we are working on that):: E + where = '123'.startswith failure_demo.py:189: AssertionError - _______ TestMoreErrors.test_startswith_nested ________ + __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -501,7 +501,7 @@ get on the terminal - we are working on that):: E + and '456' = .g at 0xdeadbeef>() failure_demo.py:196: AssertionError - _______ TestMoreErrors.test_global_func ________ + _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -512,7 +512,7 @@ get on the terminal - we are working on that):: E + where 43 = globf(42) failure_demo.py:199: AssertionError - _______ TestMoreErrors.test_instance ________ + _______________________ TestMoreErrors.test_instance _______________________ self = @@ -523,7 +523,7 @@ get on the terminal - we are working on that):: E + where 42 = .x failure_demo.py:203: AssertionError - _______ TestMoreErrors.test_compare ________ + _______________________ TestMoreErrors.test_compare ________________________ self = @@ -533,7 +533,7 @@ get on the terminal - we are working on that):: E + where 11 = globf(10) failure_demo.py:206: AssertionError - _______ TestMoreErrors.test_try_finally ________ + _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -544,7 +544,7 @@ get on the terminal - we are working on that):: E assert 1 == 0 failure_demo.py:211: AssertionError - _______ TestCustomAssertMsg.test_single_line ________ + ___________________ TestCustomAssertMsg.test_single_line ___________________ self = @@ -558,7 +558,7 @@ get on the terminal - we are working on that):: E + where 1 = .A'>.a failure_demo.py:222: AssertionError - _______ TestCustomAssertMsg.test_multiline ________ + ____________________ TestCustomAssertMsg.test_multiline ____________________ self = @@ -575,7 +575,7 @@ get on the terminal - we are working on that):: E + where 1 = .A'>.a failure_demo.py:228: AssertionError - _______ TestCustomAssertMsg.test_custom_repr ________ + ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = @@ -595,4 +595,10 @@ get on the terminal - we are working on that):: E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a failure_demo.py:238: AssertionError - ======= 42 failed in 0.12 seconds ======== + ============================= warnings summary ============================= + None + Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0. + Please use Metafunc.parametrize instead. + + -- Docs: http://doc.pytest.org/en/latest/warnings.html + ================== 42 failed, 1 warnings in 0.12 seconds =================== diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 823474095..678a0db00 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -41,9 +41,9 @@ provide the ``cmdopt`` through a :ref:`fixture function `: Let's run this without supplying our new option:: $ pytest -q test_sample.py - F - ======= FAILURES ======== - _______ test_answer ________ + F [100%] + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ cmdopt = 'type1' @@ -63,9 +63,9 @@ Let's run this without supplying our new option:: And now with supplying a command line option:: $ pytest -q --cmdopt=type2 - F - ======= FAILURES ======== - _______ test_answer ________ + F [100%] + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ cmdopt = 'type2' @@ -112,12 +112,12 @@ of subprocesses close to your CPU. Running in an empty directory with the above conftest.py:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 0 items - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= .. _`excontrolskip`: @@ -166,28 +166,28 @@ We can now write a test module like this: and when running it will see a skipped "slow" test:: $ pytest -rs # "-rs" means report details on the little 's' - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - test_module.py .s - ======= short test summary info ======== + test_module.py .s [100%] + ========================= short test summary info ========================== SKIP [1] test_module.py:8: need --runslow option to run - ======= 1 passed, 1 skipped in 0.12 seconds ======== + =================== 1 passed, 1 skipped in 0.12 seconds ==================== Or run it including the ``slow`` marked test:: $ pytest --runslow - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - test_module.py .. + test_module.py .. [100%] - ======= 2 passed in 0.12 seconds ======== + ========================= 2 passed in 0.12 seconds ========================= Writing well integrated assertion helpers -------------------------------------------------- @@ -218,9 +218,9 @@ unless the ``--full-trace`` command line option is specified. Let's run our little function:: $ pytest -q test_checkconfig.py - F - ======= FAILURES ======== - _______ test_something ________ + F [100%] + ================================= FAILURES ================================= + ______________________________ test_something ______________________________ def test_something(): > checkconfig(42) @@ -305,13 +305,13 @@ It's easy to present extra information in a ``pytest`` run: which will add the string to the test header accordingly:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y project deps: mylib-1.1 rootdir: $REGENDOC_TMPDIR, inifile: collected 0 items - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= .. regendoc:wipe @@ -330,7 +330,7 @@ display more information if applicable: which will add info only when run with "--v":: $ pytest -v - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache info1: did you know that ... @@ -338,17 +338,17 @@ which will add info only when run with "--v":: rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 0 items - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= and nothing when run plainly:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 0 items - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= profiling test duration -------------------------- @@ -377,18 +377,18 @@ out which tests are the slowest. Let's make an artificial test suite: Now we can profile which test functions execute the slowest:: $ pytest --durations=3 - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items - test_some_are_slow.py ... + test_some_are_slow.py ... [100%] - ======= slowest 3 test durations ======== + ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast - ======= 3 passed in 0.12 seconds ======== + ========================= 3 passed in 0.12 seconds ========================= incremental testing - test steps --------------------------------------------------- @@ -443,18 +443,18 @@ tests in a class. Here is a test module example: If we run this:: $ pytest -rx - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 4 items - test_step.py .Fx. - ======= short test summary info ======== + test_step.py .Fx. [100%] + ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::()::test_deletion reason: previous test failed (test_modification) - ======= FAILURES ======== - _______ TestUserHandling.test_modification ________ + ================================= FAILURES ================================= + ____________________ TestUserHandling.test_modification ____________________ self = @@ -463,7 +463,7 @@ If we run this:: E assert 0 test_step.py:9: AssertionError - ======= 1 failed, 2 passed, 1 xfailed in 0.12 seconds ======== + ============== 1 failed, 2 passed, 1 xfailed in 0.12 seconds =============== We'll see that ``test_deletion`` was not executed because ``test_modification`` failed. It is reported as an "expected failure". @@ -522,27 +522,27 @@ the ``db`` fixture: We can run this:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 7 items - test_step.py .Fx. - a/test_db.py F - a/test_db2.py F - b/test_error.py E + test_step.py .Fx. [ 57%] + a/test_db.py F [ 71%] + a/test_db2.py F [ 85%] + b/test_error.py E [100%] - ======= ERRORS ======== - _______ ERROR at setup of test_root ________ + ================================== ERRORS ================================== + _______________________ ERROR at setup of test_root ________________________ file $REGENDOC_TMPDIR/b/test_error.py, line 1 def test_root(db): # no db here, will error out E fixture 'db' not found - > available fixtures: cache, capfd, capsys, doctest_namespace, monkeypatch, pytestconfig, record_xml_property, recwarn, tmpdir, tmpdir_factory + > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_property, recwarn, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. $REGENDOC_TMPDIR/b/test_error.py:1 - ======= FAILURES ======== - _______ TestUserHandling.test_modification ________ + ================================= FAILURES ================================= + ____________________ TestUserHandling.test_modification ____________________ self = @@ -551,7 +551,7 @@ We can run this:: E assert 0 test_step.py:9: AssertionError - _______ test_a1 ________ + _________________________________ test_a1 __________________________________ db = @@ -561,7 +561,7 @@ We can run this:: E assert 0 a/test_db.py:2: AssertionError - _______ test_a2 ________ + _________________________________ test_a2 __________________________________ db = @@ -571,7 +571,7 @@ We can run this:: E assert 0 a/test_db2.py:2: AssertionError - ======= 3 failed, 2 passed, 1 xfailed, 1 error in 0.12 seconds ======== + ========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.12 seconds ========== The two test modules in the ``a`` directory see the same ``db`` fixture instance while the one test in the sister-directory ``b`` doesn't see it. We could of course @@ -630,15 +630,15 @@ if you then have failing tests: and run them:: $ pytest test_module.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - test_module.py FF + test_module.py FF [100%] - ======= FAILURES ======== - _______ test_fail1 ________ + ================================= FAILURES ================================= + ________________________________ test_fail1 ________________________________ tmpdir = local('PYTEST_TMPDIR/test_fail10') @@ -647,14 +647,14 @@ and run them:: E assert 0 test_module.py:2: AssertionError - _______ test_fail2 ________ + ________________________________ test_fail2 ________________________________ def test_fail2(): > assert 0 E assert 0 test_module.py:4: AssertionError - ======= 2 failed in 0.12 seconds ======== + ========================= 2 failed in 0.12 seconds ========================= you will have a "failures" file which contains the failing test ids:: @@ -724,17 +724,17 @@ if you then have failing tests: and run it:: $ pytest -s test_module.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items test_module.py Esetting up a test failed! test_module.py::test_setup_fails Fexecuting test failed test_module.py::test_call_fails - F + F [100%] - ======= ERRORS ======== - _______ ERROR at setup of test_setup_fails ________ + ================================== ERRORS ================================== + ____________________ ERROR at setup of test_setup_fails ____________________ @pytest.fixture def other(): @@ -742,8 +742,8 @@ and run it:: E assert 0 test_module.py:6: AssertionError - ======= FAILURES ======== - _______ test_call_fails ________ + ================================= FAILURES ================================= + _____________________________ test_call_fails ______________________________ something = None @@ -752,14 +752,14 @@ and run it:: E assert 0 test_module.py:12: AssertionError - _______ test_fail2 ________ + ________________________________ test_fail2 ________________________________ def test_fail2(): > assert 0 E assert 0 test_module.py:15: AssertionError - ======= 2 failed, 1 error in 0.12 seconds ======== + ==================== 2 failed, 1 error in 0.12 seconds ===================== You'll see that the fixture finalizers could use the precise reporting information. diff --git a/doc/en/example/special.rst b/doc/en/example/special.rst index 1fc32f6c8..4437e1cc3 100644 --- a/doc/en/example/special.rst +++ b/doc/en/example/special.rst @@ -68,5 +68,5 @@ If you run this without output capturing:: .test_method1 called .test other .test_unit1 method called - . + . [100%] 4 passed in 0.12 seconds diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index 1d7ba8640..01a941ddf 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -69,15 +69,15 @@ will discover and call the :py:func:`@pytest.fixture <_pytest.python.fixture>` marked ``smtp`` fixture function. Running the test looks like this:: $ pytest test_smtpsimple.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - test_smtpsimple.py F + test_smtpsimple.py F [100%] - ======= FAILURES ======== - _______ test_ehlo ________ + ================================= FAILURES ================================= + ________________________________ test_ehlo _________________________________ smtp = @@ -88,7 +88,7 @@ marked ``smtp`` fixture function. Running the test looks like this:: E assert 0 test_smtpsimple.py:11: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================= 1 failed in 0.12 seconds ========================= In the failure traceback we see that the test function was called with a ``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture @@ -205,15 +205,15 @@ We deliberately insert failing ``assert 0`` statements in order to inspect what is going on and can now run the tests:: $ pytest test_module.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - test_module.py FF + test_module.py FF [100%] - ======= FAILURES ======== - _______ test_ehlo ________ + ================================= FAILURES ================================= + ________________________________ test_ehlo _________________________________ smtp = @@ -225,7 +225,7 @@ inspect what is going on and can now run the tests:: E assert 0 test_module.py:6: AssertionError - _______ test_noop ________ + ________________________________ test_noop _________________________________ smtp = @@ -236,7 +236,7 @@ inspect what is going on and can now run the tests:: E assert 0 test_module.py:11: AssertionError - ======= 2 failed in 0.12 seconds ======== + ========================= 2 failed in 0.12 seconds ========================= You see the two ``assert 0`` failing and more importantly you can also see that the same (module-scoped) ``smtp`` object was passed into the two @@ -286,7 +286,7 @@ tests. Let's execute it:: $ pytest -s -q --tb=no - FFteardown smtp + FF [100%]teardown smtp 2 failed in 0.12 seconds @@ -391,7 +391,7 @@ We use the ``request.module`` attribute to optionally obtain an again, nothing much has changed:: $ pytest -s -q --tb=no - FFfinalizing (smtp.gmail.com) + FF [100%]finalizing (smtp.gmail.com) 2 failed in 0.12 seconds @@ -408,9 +408,9 @@ server URL in its module namespace:: Running it:: $ pytest -qq --tb=short test_anothersmtp.py - F - ======= FAILURES ======== - _______ test_showhelo ________ + F [100%] + ================================= FAILURES ================================= + ______________________________ test_showhelo _______________________________ test_anothersmtp.py:5: in test_showhelo assert 0, smtp.helo() E AssertionError: (250, b'mail.python.org') @@ -457,9 +457,9 @@ a value via ``request.param``. No test function code needs to change. So let's just do another run:: $ pytest -q test_module.py - FFFF - ======= FAILURES ======== - _______ test_ehlo[smtp.gmail.com] ________ + FFFF [100%] + ================================= FAILURES ================================= + ________________________ test_ehlo[smtp.gmail.com] _________________________ smtp = @@ -471,7 +471,7 @@ So let's just do another run:: E assert 0 test_module.py:6: AssertionError - _______ test_noop[smtp.gmail.com] ________ + ________________________ test_noop[smtp.gmail.com] _________________________ smtp = @@ -482,7 +482,7 @@ So let's just do another run:: E assert 0 test_module.py:11: AssertionError - _______ test_ehlo[mail.python.org] ________ + ________________________ test_ehlo[mail.python.org] ________________________ smtp = @@ -495,7 +495,7 @@ So let's just do another run:: test_module.py:5: AssertionError -------------------------- Captured stdout setup --------------------------- finalizing - _______ test_noop[mail.python.org] ________ + ________________________ test_noop[mail.python.org] ________________________ smtp = @@ -559,7 +559,7 @@ return ``None`` then pytest's auto-generated ID will be used. Running the above tests results in the following test IDs being used:: $ pytest --collect-only - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 10 items @@ -577,7 +577,7 @@ Running the above tests results in the following test IDs being used:: - ======= no tests ran in 0.12 seconds ======== + ======================= no tests ran in 0.12 seconds ======================= .. _`interdependent fixtures`: @@ -610,16 +610,16 @@ Here we declare an ``app`` fixture which receives the previously defined ``smtp`` fixture and instantiates an ``App`` object with it. Let's run it:: $ pytest -v test_appsetup.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: collecting ... collected 2 items - test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED - test_appsetup.py::test_smtp_exists[mail.python.org] PASSED + test_appsetup.py::test_smtp_exists[smtp.gmail.com] PASSED [ 50%] + test_appsetup.py::test_smtp_exists[mail.python.org] PASSED [100%] - ======= 2 passed in 0.12 seconds ======== + ========================= 2 passed in 0.12 seconds ========================= Due to the parametrization of ``smtp`` the test will run twice with two different ``App`` instances and respective smtp servers. There is no @@ -679,7 +679,7 @@ to show the setup/teardown flow:: Let's run the tests in verbose mode and with looking at the print-output:: $ pytest -v -s test_module.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5 cachedir: .cache rootdir: $REGENDOC_TMPDIR, inifile: @@ -687,38 +687,38 @@ Let's run the tests in verbose mode and with looking at the print-output:: test_module.py::test_0[1] SETUP otherarg 1 RUN test0 with otherarg 1 - PASSED TEARDOWN otherarg 1 + PASSED [ 12%] TEARDOWN otherarg 1 test_module.py::test_0[2] SETUP otherarg 2 RUN test0 with otherarg 2 - PASSED TEARDOWN otherarg 2 + PASSED [ 25%] TEARDOWN otherarg 2 test_module.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 - PASSED + PASSED [ 37%] test_module.py::test_2[1-mod1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 - PASSED TEARDOWN otherarg 1 + PASSED [ 50%] TEARDOWN otherarg 1 test_module.py::test_2[2-mod1] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 - PASSED TEARDOWN otherarg 2 + PASSED [ 62%] TEARDOWN otherarg 2 test_module.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 - PASSED + PASSED [ 75%] test_module.py::test_2[1-mod2] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 - PASSED TEARDOWN otherarg 1 + PASSED [ 87%] TEARDOWN otherarg 1 test_module.py::test_2[2-mod2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 - PASSED TEARDOWN otherarg 2 + PASSED [100%] TEARDOWN otherarg 2 TEARDOWN modarg mod2 - ======= 8 passed in 0.12 seconds ======== + ========================= 8 passed in 0.12 seconds ========================= You can see that the parametrized module-scoped ``modarg`` resource caused an ordering of test execution that lead to the fewest possible "active" resources. @@ -781,7 +781,7 @@ you specified a "cleandir" function argument to each of them. Let's run it to verify our fixture is activated and the tests pass:: $ pytest -q - .. + .. [100%] 2 passed in 0.12 seconds You can specify multiple fixtures like this: @@ -862,7 +862,7 @@ class-level ``usefixtures`` decorator. If we run it, we get two passing tests:: $ pytest -q - .. + .. [100%] 2 passed in 0.12 seconds Here is how autouse fixtures work in other scopes: diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index d2eb77d3e..64b010826 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -44,23 +44,23 @@ Let's create a first test file with a simple test function:: That's it. You can execute the test function now:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - - test_sample.py F - - ======= FAILURES ======== - _______ test_answer ________ - + + test_sample.py F [100%] + + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ + def test_answer(): > assert func(3) == 5 E assert 4 == 5 E + where 4 = func(3) - + test_sample.py:5: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================= 1 failed in 0.12 seconds ========================= We got a failure report because our little ``func(3)`` call did not return ``5``. @@ -99,7 +99,7 @@ use the ``raises`` helper:: Running it with, this time in "quiet" reporting mode:: $ pytest -q test_sysexit.py - . + . [100%] 1 passed in 0.12 seconds Grouping multiple tests in a class @@ -124,18 +124,18 @@ There is no need to subclass anything. We can simply run the module by passing its filename:: $ pytest -q test_class.py - .F - ======= FAILURES ======== - _______ TestClass.test_two ________ - + .F [100%] + ================================= FAILURES ================================= + ____________________________ TestClass.test_two ____________________________ + self = - + def test_two(self): x = "hello" > assert hasattr(x, 'check') E AssertionError: assert False E + where False = hasattr('hello', 'check') - + test_class.py:8: AssertionError 1 failed, 1 passed in 0.12 seconds @@ -161,17 +161,17 @@ We list the name ``tmpdir`` in the test function signature and before performing the test function call. Let's just run it:: $ pytest -q test_tmpdir.py - F - ======= FAILURES ======== - _______ test_needsfiles ________ - + F [100%] + ================================= FAILURES ================================= + _____________________________ test_needsfiles ______________________________ + tmpdir = local('PYTEST_TMPDIR/test_needsfiles0') - + def test_needsfiles(tmpdir): print (tmpdir) > assert 0 E assert 0 - + test_tmpdir.py:3: AssertionError --------------------------- Captured stdout call --------------------------- PYTEST_TMPDIR/test_needsfiles0 diff --git a/doc/en/index.rst b/doc/en/index.rst index d9414a076..66c59f08d 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -24,23 +24,23 @@ An example of a simple test: To execute it:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - - test_sample.py F - - ======= FAILURES ======== - _______ test_answer ________ - + + test_sample.py F [100%] + + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ + def test_answer(): > assert inc(3) == 5 E assert 4 == 5 E + where 4 = inc(3) - + test_sample.py:5: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================= 1 failed in 0.12 seconds ========================= Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See :ref:`Getting Started ` for more examples. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index ebbae31b2..7a4ac2e18 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -53,15 +53,15 @@ tuples so that the ``test_eval`` function will run three times using them in turn:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items - test_expectation.py ..F + test_expectation.py ..F [100%] - ======= FAILURES ======== - _______ test_eval[6*9-42] ________ + ================================= FAILURES ================================= + ____________________________ test_eval[6*9-42] _____________________________ test_input = '6*9', expected = 42 @@ -76,7 +76,7 @@ them in turn:: E + where 54 = eval('6*9') test_expectation.py:8: AssertionError - ======= 1 failed, 2 passed in 0.12 seconds ======== + ==================== 1 failed, 2 passed in 0.12 seconds ==================== As designed in this example, only one pair of input/output values fails the simple test function. And as usual with test function arguments, @@ -102,14 +102,14 @@ for example with the builtin ``mark.xfail``:: Let's run this:: $ pytest - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 3 items - test_expectation.py ..x + test_expectation.py ..x [100%] - ======= 2 passed, 1 xfailed in 0.12 seconds ======== + =================== 2 passed, 1 xfailed in 0.12 seconds ==================== The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. @@ -165,15 +165,15 @@ command line option and the parametrization of our test function:: If we now pass two stringinput values, our test will run twice:: $ pytest -q --stringinput="hello" --stringinput="world" test_strings.py - .. + .. [100%] 2 passed in 0.12 seconds Let's also run with a stringinput that will lead to a failing test:: $ pytest -q --stringinput="!" test_strings.py - F - ======= FAILURES ======== - _______ test_valid_string[!] ________ + F [100%] + ================================= FAILURES ================================= + ___________________________ test_valid_string[!] ___________________________ stringinput = '!' @@ -193,9 +193,9 @@ If you don't specify a stringinput it will be skipped because list:: $ pytest -q -rs test_strings.py - s - ======= short test summary info ======== - SKIP [1] test_strings.py:2: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:1 + s [100%] + ========================= short test summary info ========================== + SKIP [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:1 1 skipped in 0.12 seconds Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 37252d7f5..7e001929b 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -331,13 +331,13 @@ Here is a simple test file with the several usages: Running it with the report-on-xfail option gives this output:: example $ pytest -rx xfail_demo.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR/example, inifile: collected 7 items - xfail_demo.py xxxxxxx - ======= short test summary info ======== + xfail_demo.py xxxxxxx [100%] + ========================= short test summary info ========================== XFAIL xfail_demo.py::test_hello XFAIL xfail_demo.py::test_hello2 reason: [NOTRUN] @@ -351,7 +351,7 @@ Running it with the report-on-xfail option gives this output:: reason: reason XFAIL xfail_demo.py::test_hello7 - ======= 7 xfailed in 0.12 seconds ======== + ======================== 7 xfailed in 0.12 seconds ========================= .. _`skip/xfail with parametrize`: diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 56a347619..b8174484e 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -28,15 +28,15 @@ Running this would result in a passed test except for the last ``assert 0`` line which we use to look at values:: $ pytest test_tmpdir.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - test_tmpdir.py F + test_tmpdir.py F [100%] - ======= FAILURES ======== - _______ test_create_file ________ + ================================= FAILURES ================================= + _____________________________ test_create_file _____________________________ tmpdir = local('PYTEST_TMPDIR/test_create_file0') @@ -49,7 +49,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmpdir.py:7: AssertionError - ======= 1 failed in 0.12 seconds ======== + ========================= 1 failed in 0.12 seconds ========================= The 'tmpdir_factory' fixture ---------------------------- diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index db1692029..b44bda44f 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -126,15 +126,15 @@ Due to the deliberately failing assert statements, we can take a look at the ``self.db`` values in the traceback:: $ pytest test_unittest_db.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 2 items - test_unittest_db.py FF + test_unittest_db.py FF [100%] - ======= FAILURES ======== - _______ MyTest.test_method1 ________ + ================================= FAILURES ================================= + ___________________________ MyTest.test_method1 ____________________________ self = @@ -145,7 +145,7 @@ the ``self.db`` values in the traceback:: E assert 0 test_unittest_db.py:9: AssertionError - _______ MyTest.test_method2 ________ + ___________________________ MyTest.test_method2 ____________________________ self = @@ -155,7 +155,7 @@ the ``self.db`` values in the traceback:: E assert 0 test_unittest_db.py:12: AssertionError - ======= 2 failed in 0.12 seconds ======== + ========================= 2 failed in 0.12 seconds ========================= This default pytest traceback shows that the two test methods share the same ``self.db`` instance which was our intention @@ -203,7 +203,7 @@ on the class like in the previous example. Running this test module ...:: $ pytest -q test_unittest_cleandir.py - . + . [100%] 1 passed in 0.12 seconds ... gives us one passed test because the ``initdir`` fixture function diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 87faeb7bd..f249d7e3b 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -21,20 +21,20 @@ and displays them at the end of the session:: Running pytest now produces this output:: $ pytest test_show_warnings.py - ======= test session starts ======== + =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item - test_show_warnings.py . + test_show_warnings.py . [100%] - ======= warnings summary ======== + ============================= warnings summary ============================= test_show_warnings.py::test_one $REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2 warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: http://doc.pytest.org/en/latest/warnings.html - ======= 1 passed, 1 warnings in 0.12 seconds ======== + =================== 1 passed, 1 warnings in 0.12 seconds =================== Pytest by default catches all warnings except for ``DeprecationWarning`` and ``PendingDeprecationWarning``. @@ -42,9 +42,9 @@ The ``-W`` flag can be passed to control which warnings will be displayed or eve them into errors:: $ pytest -q test_show_warnings.py -W error::UserWarning - F - ======= FAILURES ======== - _______ test_one ________ + F [100%] + ================================= FAILURES ================================= + _________________________________ test_one _________________________________ def test_one(): > assert api_v1() == 1 From e97c774f8edd6f92deafaf65571a9149ecebce94 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Nov 2017 20:23:50 +0000 Subject: [PATCH 152/159] Remove vendoring import from tasks/__init__ --- tasks/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tasks/__init__.py b/tasks/__init__.py index 992f4a4ad..8ea038f0a 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -4,10 +4,9 @@ Invoke tasks to help with pytest development and release process. import invoke -from . import generate, vendoring +from . import generate ns = invoke.Collection( generate, - vendoring ) From aa9a02ec4452f87b40c10a2c9235e4b552b68564 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Nov 2017 20:25:36 +0000 Subject: [PATCH 153/159] Preparing release version 3.3.0 --- CHANGELOG.rst | 175 ++++++++++++++++++++++++++++++ changelog/1993.bugfix | 1 - changelog/2124.bugfix | 1 - changelog/2124.feature | 1 - changelog/2236.removal | 1 - changelog/2267.feature | 4 - changelog/2491.bugfix | 4 - changelog/2549.feature | 1 - changelog/2641.trivial | 1 - changelog/2642.trivial | 1 - changelog/2657.feature | 1 - changelog/2672.removal | 2 - changelog/2675.removal | 1 - changelog/2708.feature | 1 - changelog/2709.bugfix | 1 - changelog/2719.trivial | 1 - changelog/2734.trivial | 1 - changelog/2738.bugfix | 1 - changelog/2751.bugfix | 1 - changelog/2767.removal | 1 - changelog/2767.trivial | 1 - changelog/2794.feature | 3 - changelog/2803.removal | 1 - changelog/2808.feature | 1 - changelog/2809.bugfix | 1 - changelog/2812.removal | 1 - changelog/2824.feature | 1 - changelog/2845.bugfix | 1 - changelog/2876.trivial | 1 - changelog/2877.trivial | 1 - changelog/2879.feature | 1 - changelog/2910.trivial | 1 - changelog/2922.trivial | 1 - changelog/2923.feature | 2 - changelog/2934.feature | 2 - changelog/2939.bugfix | 1 - changelog/2942.bugfix | 1 - changelog/502.feature | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-3.3.0.rst | 50 +++++++++ doc/en/example/parametrize.rst | 7 +- 41 files changed, 228 insertions(+), 53 deletions(-) delete mode 100644 changelog/1993.bugfix delete mode 100644 changelog/2124.bugfix delete mode 100644 changelog/2124.feature delete mode 100644 changelog/2236.removal delete mode 100644 changelog/2267.feature delete mode 100644 changelog/2491.bugfix delete mode 100644 changelog/2549.feature delete mode 100644 changelog/2641.trivial delete mode 100644 changelog/2642.trivial delete mode 100644 changelog/2657.feature delete mode 100644 changelog/2672.removal delete mode 100644 changelog/2675.removal delete mode 100644 changelog/2708.feature delete mode 100644 changelog/2709.bugfix delete mode 100644 changelog/2719.trivial delete mode 100644 changelog/2734.trivial delete mode 100644 changelog/2738.bugfix delete mode 100644 changelog/2751.bugfix delete mode 100644 changelog/2767.removal delete mode 100644 changelog/2767.trivial delete mode 100644 changelog/2794.feature delete mode 100644 changelog/2803.removal delete mode 100644 changelog/2808.feature delete mode 100644 changelog/2809.bugfix delete mode 100644 changelog/2812.removal delete mode 100644 changelog/2824.feature delete mode 100644 changelog/2845.bugfix delete mode 100644 changelog/2876.trivial delete mode 100644 changelog/2877.trivial delete mode 100644 changelog/2879.feature delete mode 100644 changelog/2910.trivial delete mode 100644 changelog/2922.trivial delete mode 100644 changelog/2923.feature delete mode 100644 changelog/2934.feature delete mode 100644 changelog/2939.bugfix delete mode 100644 changelog/2942.bugfix delete mode 100644 changelog/502.feature create mode 100644 doc/en/announce/release-3.3.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4146670f2..fe3ff47e2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,181 @@ .. towncrier release notes start +Pytest 3.3.0 (2017-11-23) +========================= + +Deprecations and Removals +------------------------- + +- - Remove internal ``_preloadplugins()`` function. This removal is part of the + ``pytest_namespace()`` hook deprecation. (`#2236 + `_) + +- Internally change ``CallSpec2`` to have a list of marks instead of a broken + mapping of keywords. This removes the keywords attribute of the internal + ``CallSpec2`` class. (`#2672 + `_) + +- remove ParameterSet.deprecated_arg_dict - its not a public api and the lack + of the underscore was a naming error. (`#2675 + `_) + +- Remove the internal multi-typed attribute ``Node._evalskip`` and replace it + with the boolean ``Node._skipped_by_mark``. (`#2767 + `_) + +- ``TerminalReporter._tw`` has been deprecated in favor of + ``TerminalReporter.writer`` and will be removed in a future version. Also, + ``TerminalReporter.writer`` is now read-only. (`#2803 + `_) + +- Pytest no longer supports Python **2.6** and **3.3**. Those Python versions + are EOL for some time now and incurr maintanance and compatibility costs on + the pytest core team, and following up with the rest of the community we + decided that they will no longer be supported starting on this version. Users + which still require those versions should pin pytest to ``<3.3``. (`#2812 + `_) + + +Features +-------- + +- ``pytest_fixture_post_finalizer`` hook can now receive a ``request`` + argument. (`#2124 `_) + +- Replace the old introspection code in compat.py that determines the available + arguments of fixtures with inspect.signature on Python 3 and + funcsigs.signature on Python 2. This should respect __signature__ + declarations on functions. (`#2267 + `_) + +- Report only once tests with global ``pytestmark`` variable. (`#2549 + `_) + +- Now pytest displays the total progress percentage while running tests. The + previous output style can be set by setting the new ``console_output_style`` + to ``classic``. (`#2657 `_) + +- Match ``warns`` signature to ``raises`` by adding ``match`` keyworkd. (`#2708 + `_) + +- Pytest now captures and displays output from the standard `logging` module. + The user can control the logging level to be captured by specifying options + in ``pytest.ini``, the command line and also during individual tests using + markers. Also, a ``caplog`` fixture is available that enables users to test + the captured log during specific tests (similar to ``capsys`` for example). + For more information, please see the `docs + `_. This feature was + introduced by merging the popular `pytest-catchlog + `_ plugin, thanks to `Thomas Hisch + `_. Be advised that during the merging the + backward compatibility interface with the defunct ``pytest-capturelog`` has + been dropped. (`#2794 `_) + +- Add ``allow_module_level`` kwarg to ``pytest.skip()``, enabling to skip the + whole module. (`#2808 `_) + +- Allow setting ``file_or_dir``, ``-c``, and ``-o`` in PYTEST_ADDOPTS. (`#2824 + `_) + +- Return stdout/stderr capture results as a ``namedtuple``, so ``out`` and + ``err`` can be accessed by attribute. (`#2879 + `_) + +- Add ``capfdbinary`` a version of ``capfd`` which returns bytes from + ``readouterr()``. (`#2923 + `_) + +- Add ``capsysbinary`` a version of ``capsys`` which returns bytes from + ``readouterr()``. (`#2934 + `_) + +- Implement feature to skip ``setup.py`` files when ran with + ``--doctest-modules``. (`#502 + `_) + + +Bug Fixes +--------- + +- Resume output capturing after ``capsys/capfd.disabled()`` context manager. + (`#1993 `_) + +- ``pytest_fixture_setup`` and ``pytest_fixture_post_finalizer`` hooks are now + called for all ``conftest.py`` files. (`#2124 + `_) + +- If an exception happens while loading a plugin, pytest no longer hides the + original traceback. In python2 it will show the original traceback with a new + message that explains in which plugin. In python3 it will show 2 canonized + exceptions, the original exception while loading the plugin in addition to an + exception that PyTest throws about loading a plugin. (`#2491 + `_) + +- ``capsys`` and ``capfd`` can now be used by other fixtures. (`#2709 + `_) + +- Internal ``pytester`` plugin properly encodes ``bytes`` arguments to + ``utf-8``. (`#2738 `_) + +- ``testdir`` now uses use the same method used by ``tmpdir`` to create its + temporary directory. This changes the final structure of the ``testdir`` + directory slightly, but should not affect usage in normal scenarios and + avoids a number of potential problems. (`#2751 + `_) + +- Pytest no longer complains about warnings with unicode messages being + non-ascii compatible even for ascii-compatible messages. As a result of this, + warnings with unicode messages are converted first to an ascii representation + for safety. (`#2809 `_) + +- Change return value of pytest command when ``--maxfail`` is reached from + ``2`` (interrupted) to ``1`` (failed). (`#2845 + `_) + +- Fix issue in assertion rewriting which could lead it to rewrite modules which + should not be rewritten. (`#2939 + `_) + +- Handle marks without description in ``pytest.ini``. (`#2942 + `_) + + +Trivial/Internal Changes +------------------------ + +- pytest now depends on `attrs `_ for internal + structures to ease code maintainability. (`#2641 + `_) + +- Refactored internal Python 2/3 compatibility code to use ``six``. (`#2642 + `_) + +- Stop vendoring ``pluggy`` - we're missing out on it's latest changes for not + much benefit (`#2719 `_) + +- Internal refactor: simplify ascii string escaping by using the + backslashreplace error handler in newer Python 3 versions. (`#2734 + `_) + +- * remove unnecessary mark evaluator in unittest plugin (`#2767 + `_) + +- Calls to ``Metafunc.addcall`` now emit a deprecation warning. This function + is scheduled to be removed in ``pytest-4.0``. (`#2876 + `_) + +- Internal move of the parameterset extraction to a more maintainable place. + (`#2877 `_) + +- Internal refactoring to simplify scope node lookup. (`#2910 + `_) + +- Configure ``pytest`` to prevent pip from installing pytest in unsupported + Python versions. (`#2922 + `_) + + Pytest 3.2.5 (2017-11-15) ========================= diff --git a/changelog/1993.bugfix b/changelog/1993.bugfix deleted file mode 100644 index 07a78cc91..000000000 --- a/changelog/1993.bugfix +++ /dev/null @@ -1 +0,0 @@ -Resume output capturing after ``capsys/capfd.disabled()`` context manager. diff --git a/changelog/2124.bugfix b/changelog/2124.bugfix deleted file mode 100644 index e1c5e044c..000000000 --- a/changelog/2124.bugfix +++ /dev/null @@ -1 +0,0 @@ -``pytest_fixture_setup`` and ``pytest_fixture_post_finalizer`` hooks are now called for all ``conftest.py`` files. diff --git a/changelog/2124.feature b/changelog/2124.feature deleted file mode 100644 index 267fdabc9..000000000 --- a/changelog/2124.feature +++ /dev/null @@ -1 +0,0 @@ -``pytest_fixture_post_finalizer`` hook can now receive a ``request`` argument. diff --git a/changelog/2236.removal b/changelog/2236.removal deleted file mode 100644 index 84f98d009..000000000 --- a/changelog/2236.removal +++ /dev/null @@ -1 +0,0 @@ -- Remove internal ``_preloadplugins()`` function. This removal is part of the ``pytest_namespace()`` hook deprecation. \ No newline at end of file diff --git a/changelog/2267.feature b/changelog/2267.feature deleted file mode 100644 index a2f14811e..000000000 --- a/changelog/2267.feature +++ /dev/null @@ -1,4 +0,0 @@ -Replace the old introspection code in compat.py that determines the -available arguments of fixtures with inspect.signature on Python 3 and -funcsigs.signature on Python 2. This should respect __signature__ -declarations on functions. diff --git a/changelog/2491.bugfix b/changelog/2491.bugfix deleted file mode 100644 index fbb16e17e..000000000 --- a/changelog/2491.bugfix +++ /dev/null @@ -1,4 +0,0 @@ -If an exception happens while loading a plugin, pytest no longer hides the original traceback. -In python2 it will show the original traceback with a new message that explains in which plugin. -In python3 it will show 2 canonized exceptions, the original exception while loading the plugin -in addition to an exception that PyTest throws about loading a plugin. diff --git a/changelog/2549.feature b/changelog/2549.feature deleted file mode 100644 index 4866d990d..000000000 --- a/changelog/2549.feature +++ /dev/null @@ -1 +0,0 @@ -Report only once tests with global ``pytestmark`` variable. \ No newline at end of file diff --git a/changelog/2641.trivial b/changelog/2641.trivial deleted file mode 100644 index 1799f90d2..000000000 --- a/changelog/2641.trivial +++ /dev/null @@ -1 +0,0 @@ -pytest now depends on `attrs `_ for internal structures to ease code maintainability. diff --git a/changelog/2642.trivial b/changelog/2642.trivial deleted file mode 100644 index bae449dba..000000000 --- a/changelog/2642.trivial +++ /dev/null @@ -1 +0,0 @@ -Refactored internal Python 2/3 compatibility code to use ``six``. diff --git a/changelog/2657.feature b/changelog/2657.feature deleted file mode 100644 index d0cc3bfd4..000000000 --- a/changelog/2657.feature +++ /dev/null @@ -1 +0,0 @@ -Now pytest displays the total progress percentage while running tests. The previous output style can be set by setting the new ``console_output_style`` to ``classic``. diff --git a/changelog/2672.removal b/changelog/2672.removal deleted file mode 100644 index e660c27fd..000000000 --- a/changelog/2672.removal +++ /dev/null @@ -1,2 +0,0 @@ -Internally change ``CallSpec2`` to have a list of marks instead of a broken mapping of keywords. -This removes the keywords attribute of the internal ``CallSpec2`` class. \ No newline at end of file diff --git a/changelog/2675.removal b/changelog/2675.removal deleted file mode 100644 index 44f597892..000000000 --- a/changelog/2675.removal +++ /dev/null @@ -1 +0,0 @@ -remove ParameterSet.deprecated_arg_dict - its not a public api and the lack of the underscore was a naming error. \ No newline at end of file diff --git a/changelog/2708.feature b/changelog/2708.feature deleted file mode 100644 index f6039ede9..000000000 --- a/changelog/2708.feature +++ /dev/null @@ -1 +0,0 @@ -Match ``warns`` signature to ``raises`` by adding ``match`` keyworkd. \ No newline at end of file diff --git a/changelog/2709.bugfix b/changelog/2709.bugfix deleted file mode 100644 index 88503b050..000000000 --- a/changelog/2709.bugfix +++ /dev/null @@ -1 +0,0 @@ -``capsys`` and ``capfd`` can now be used by other fixtures. diff --git a/changelog/2719.trivial b/changelog/2719.trivial deleted file mode 100644 index 008f1dd20..000000000 --- a/changelog/2719.trivial +++ /dev/null @@ -1 +0,0 @@ -Stop vendoring ``pluggy`` - we're missing out on it's latest changes for not much benefit diff --git a/changelog/2734.trivial b/changelog/2734.trivial deleted file mode 100644 index b3f8471af..000000000 --- a/changelog/2734.trivial +++ /dev/null @@ -1 +0,0 @@ -Internal refactor: simplify ascii string escaping by using the backslashreplace error handler in newer Python 3 versions. diff --git a/changelog/2738.bugfix b/changelog/2738.bugfix deleted file mode 100644 index c53869f49..000000000 --- a/changelog/2738.bugfix +++ /dev/null @@ -1 +0,0 @@ -Internal ``pytester`` plugin properly encodes ``bytes`` arguments to ``utf-8``. diff --git a/changelog/2751.bugfix b/changelog/2751.bugfix deleted file mode 100644 index 76004a653..000000000 --- a/changelog/2751.bugfix +++ /dev/null @@ -1 +0,0 @@ -``testdir`` now uses use the same method used by ``tmpdir`` to create its temporary directory. This changes the final structure of the ``testdir`` directory slightly, but should not affect usage in normal scenarios and avoids a number of potential problems. \ No newline at end of file diff --git a/changelog/2767.removal b/changelog/2767.removal deleted file mode 100644 index b9c3984cd..000000000 --- a/changelog/2767.removal +++ /dev/null @@ -1 +0,0 @@ -Remove the internal multi-typed attribute ``Node._evalskip`` and replace it with the boolean ``Node._skipped_by_mark``. \ No newline at end of file diff --git a/changelog/2767.trivial b/changelog/2767.trivial deleted file mode 100644 index c42a06e07..000000000 --- a/changelog/2767.trivial +++ /dev/null @@ -1 +0,0 @@ -* remove unnecessary mark evaluator in unittest plugin \ No newline at end of file diff --git a/changelog/2794.feature b/changelog/2794.feature deleted file mode 100644 index 9cff1c4bb..000000000 --- a/changelog/2794.feature +++ /dev/null @@ -1,3 +0,0 @@ -Pytest now captures and displays output from the standard `logging` module. The user can control the logging level to be captured by specifying options in ``pytest.ini``, the command line and also during individual tests using markers. Also, a ``caplog`` fixture is available that enables users to test the captured log during specific tests (similar to ``capsys`` for example). For more information, please see the `docs `_. - -This feature was introduced by merging the popular `pytest-catchlog `_ plugin, thanks to `Thomas Hisch `_. Be advised that during the merging the backward compatibility interface with the defunct ``pytest-capturelog`` has been dropped. \ No newline at end of file diff --git a/changelog/2803.removal b/changelog/2803.removal deleted file mode 100644 index 4ebdb903e..000000000 --- a/changelog/2803.removal +++ /dev/null @@ -1 +0,0 @@ -``TerminalReporter._tw`` has been deprecated in favor of ``TerminalReporter.writer`` and will be removed in a future version. Also, ``TerminalReporter.writer`` is now read-only. diff --git a/changelog/2808.feature b/changelog/2808.feature deleted file mode 100644 index 26245f047..000000000 --- a/changelog/2808.feature +++ /dev/null @@ -1 +0,0 @@ -Add ``allow_module_level`` kwarg to ``pytest.skip()``, enabling to skip the whole module. diff --git a/changelog/2809.bugfix b/changelog/2809.bugfix deleted file mode 100644 index 6db7e8c6c..000000000 --- a/changelog/2809.bugfix +++ /dev/null @@ -1 +0,0 @@ -Pytest no longer complains about warnings with unicode messages being non-ascii compatible even for ascii-compatible messages. As a result of this, warnings with unicode messages are converted first to an ascii representation for safety. \ No newline at end of file diff --git a/changelog/2812.removal b/changelog/2812.removal deleted file mode 100644 index 74894e0ef..000000000 --- a/changelog/2812.removal +++ /dev/null @@ -1 +0,0 @@ -Pytest no longer supports Python **2.6** and **3.3**. Those Python versions are EOL for some time now and incurr maintanance and compatibility costs on the pytest core team, and following up with the rest of the community we decided that they will no longer be supported starting on this version. Users which still require those versions should pin pytest to ``<3.3``. diff --git a/changelog/2824.feature b/changelog/2824.feature deleted file mode 100644 index 690ca939a..000000000 --- a/changelog/2824.feature +++ /dev/null @@ -1 +0,0 @@ -Allow setting ``file_or_dir``, ``-c``, and ``-o`` in PYTEST_ADDOPTS. diff --git a/changelog/2845.bugfix b/changelog/2845.bugfix deleted file mode 100644 index c6767631e..000000000 --- a/changelog/2845.bugfix +++ /dev/null @@ -1 +0,0 @@ -Change return value of pytest command when ``--maxfail`` is reached from ``2`` (interrupted) to ``1`` (failed). diff --git a/changelog/2876.trivial b/changelog/2876.trivial deleted file mode 100644 index 354f6ad5c..000000000 --- a/changelog/2876.trivial +++ /dev/null @@ -1 +0,0 @@ -Calls to ``Metafunc.addcall`` now emit a deprecation warning. This function is scheduled to be removed in ``pytest-4.0``. diff --git a/changelog/2877.trivial b/changelog/2877.trivial deleted file mode 100644 index c4198af97..000000000 --- a/changelog/2877.trivial +++ /dev/null @@ -1 +0,0 @@ -Internal move of the parameterset extraction to a more maintainable place. diff --git a/changelog/2879.feature b/changelog/2879.feature deleted file mode 100644 index 8932d8c30..000000000 --- a/changelog/2879.feature +++ /dev/null @@ -1 +0,0 @@ -Return stdout/stderr capture results as a ``namedtuple``, so ``out`` and ``err`` can be accessed by attribute. diff --git a/changelog/2910.trivial b/changelog/2910.trivial deleted file mode 100644 index 87bf00dfe..000000000 --- a/changelog/2910.trivial +++ /dev/null @@ -1 +0,0 @@ -Internal refactoring to simplify scope node lookup. diff --git a/changelog/2922.trivial b/changelog/2922.trivial deleted file mode 100644 index 06d1346bb..000000000 --- a/changelog/2922.trivial +++ /dev/null @@ -1 +0,0 @@ -Configure ``pytest`` to prevent pip from installing pytest in unsupported Python versions. diff --git a/changelog/2923.feature b/changelog/2923.feature deleted file mode 100644 index c6937741f..000000000 --- a/changelog/2923.feature +++ /dev/null @@ -1,2 +0,0 @@ -Add ``capfdbinary`` a version of ``capfd`` which returns bytes from -``readouterr()``. diff --git a/changelog/2934.feature b/changelog/2934.feature deleted file mode 100644 index 38dcb5f73..000000000 --- a/changelog/2934.feature +++ /dev/null @@ -1,2 +0,0 @@ -Add ``capsysbinary`` a version of ``capsys`` which returns bytes from -``readouterr()``. diff --git a/changelog/2939.bugfix b/changelog/2939.bugfix deleted file mode 100644 index 8aeedad01..000000000 --- a/changelog/2939.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix issue in assertion rewriting which could lead it to rewrite modules which should not be rewritten. diff --git a/changelog/2942.bugfix b/changelog/2942.bugfix deleted file mode 100644 index 3223719e2..000000000 --- a/changelog/2942.bugfix +++ /dev/null @@ -1 +0,0 @@ -Handle marks without description in ``pytest.ini``. diff --git a/changelog/502.feature b/changelog/502.feature deleted file mode 100644 index eb61640b9..000000000 --- a/changelog/502.feature +++ /dev/null @@ -1 +0,0 @@ -Implement feature to skip ``setup.py`` files when ran with ``--doctest-modules``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index d340f5d46..1a5f3760b 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-3.3.0 release-3.2.5 release-3.2.4 release-3.2.3 diff --git a/doc/en/announce/release-3.3.0.rst b/doc/en/announce/release-3.3.0.rst new file mode 100644 index 000000000..e0740e7d5 --- /dev/null +++ b/doc/en/announce/release-3.3.0.rst @@ -0,0 +1,50 @@ +pytest-3.3.0 +======================================= + +The pytest team is proud to announce the 3.3.0 release! + +pytest is a mature Python testing tool with more than a 1600 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + http://doc.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + http://docs.pytest.org + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Anthony Sottile +* Bruno Oliveira +* Ceridwen +* Daniel Hahler +* Dirk Thomas +* Dmitry Malinovsky +* Florian Bruhin +* George Y. Kussumoto +* Hugo +* Jesús Espino +* Joan Massich +* Ofir +* OfirOshir +* Ronny Pfannschmidt +* Samuel Dion-Girardeau +* Srinivas Reddy Thatiparthy +* Sviatoslav Abakumov +* Tarcisio Fischer +* Thomas Hisch +* Tyler Goodlet +* hugovk +* je +* prokaktus + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 49852ed7b..dd01b2527 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -411,11 +411,8 @@ is to be run with different sets of arguments for its three arguments: Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize):: . $ pytest -rs -q multipython.py - ssssssssssssssssssssssss... [100%] - ========================= short test summary info ========================== - SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:24: 'python2.7' not found - SKIP [12] $REGENDOC_TMPDIR/CWD/multipython.py:24: 'python3.4' not found - 3 passed, 24 skipped in 0.12 seconds + ........................... [100%] + 27 passed in 0.12 seconds Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- From 0ddd3e2839ec32690e379b9e2ee6962b027ab52c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Nov 2017 18:56:24 -0200 Subject: [PATCH 154/159] Fix linting in CHANGELOG --- CHANGELOG.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fe3ff47e2..e4b34ff5c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ Pytest 3.3.0 (2017-11-23) Deprecations and Removals ------------------------- -- - Remove internal ``_preloadplugins()`` function. This removal is part of the +- Remove internal ``_preloadplugins()`` function. This removal is part of the ``pytest_namespace()`` hook deprecation. (`#2236 `_) @@ -71,7 +71,7 @@ Features in ``pytest.ini``, the command line and also during individual tests using markers. Also, a ``caplog`` fixture is available that enables users to test the captured log during specific tests (similar to ``capsys`` for example). - For more information, please see the `docs + For more information, please see the `logging docs `_. This feature was introduced by merging the popular `pytest-catchlog `_ plugin, thanks to `Thomas Hisch @@ -165,7 +165,7 @@ Trivial/Internal Changes backslashreplace error handler in newer Python 3 versions. (`#2734 `_) -- * remove unnecessary mark evaluator in unittest plugin (`#2767 +- Remove unnecessary mark evaluator in unittest plugin (`#2767 `_) - Calls to ``Metafunc.addcall`` now emit a deprecation warning. This function @@ -367,7 +367,7 @@ Deprecations and Removals ------------------------- - ``pytest.approx`` no longer supports ``>``, ``>=``, ``<`` and ``<=`` - operators to avoid surprising/inconsistent behavior. See `the docs + operators to avoid surprising/inconsistent behavior. See `the approx docs `_ for more information. (`#2003 `_) From 05cfdcc8cba7232724bc70d30337e87092de2818 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Nov 2017 19:26:57 -0200 Subject: [PATCH 155/159] Revert making TerminalWriter public in TerminalReporter plugin We eventually want to replace py.io.TerminalWriter (exposed as "_tw" in TerminalReporter) by something else, so deprecating access to "_tw" and making it public as "writer" is contrary to that. --- CHANGELOG.rst | 5 -- _pytest/config.py | 2 +- _pytest/debugging.py | 2 +- _pytest/helpconfig.py | 2 +- _pytest/pastebin.py | 6 +-- _pytest/skipping.py | 4 +- _pytest/terminal.py | 99 ++++++++++++++++++---------------------- testing/test_mark.py | 2 +- testing/test_terminal.py | 2 +- 9 files changed, 54 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4b34ff5c..2779761ed 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,11 +31,6 @@ Deprecations and Removals with the boolean ``Node._skipped_by_mark``. (`#2767 `_) -- ``TerminalReporter._tw`` has been deprecated in favor of - ``TerminalReporter.writer`` and will be removed in a future version. Also, - ``TerminalReporter.writer`` is now read-only. (`#2803 - `_) - - Pytest no longer supports Python **2.6** and **3.3**. Those Python versions are EOL for some time now and incurr maintanance and compatibility costs on the pytest core team, and following up with the rest of the community we diff --git a/_pytest/config.py b/_pytest/config.py index b65ce9a51..0343951da 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -931,7 +931,7 @@ class Config(object): fslocation=fslocation, nodeid=nodeid)) def get_terminal_writer(self): - return self.pluginmanager.get_plugin("terminalreporter").writer + return self.pluginmanager.get_plugin("terminalreporter")._tw def pytest_cmdline_parse(self, pluginmanager, args): # REF1 assert self == pluginmanager.config, (self, pluginmanager.config) diff --git a/_pytest/debugging.py b/_pytest/debugging.py index a67752751..d7dca7809 100644 --- a/_pytest/debugging.py +++ b/_pytest/debugging.py @@ -83,7 +83,7 @@ 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").writer + tw = node.config.pluginmanager.getplugin("terminalreporter")._tw tw.line() tw.sep(">", "traceback") rep.toterminal(tw) diff --git a/_pytest/helpconfig.py b/_pytest/helpconfig.py index 8438c3004..e744637f8 100644 --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -107,7 +107,7 @@ def pytest_cmdline_main(config): def showhelp(config): reporter = config.pluginmanager.get_plugin('terminalreporter') - tw = reporter.writer + tw = reporter._tw tw.write(config._parser.optparser.format_help()) tw.line() tw.line() diff --git a/_pytest/pastebin.py b/_pytest/pastebin.py index 68aa331f7..b588b021b 100644 --- a/_pytest/pastebin.py +++ b/_pytest/pastebin.py @@ -25,7 +25,7 @@ def pytest_configure(config): if tr is not None: # pastebin file will be utf-8 encoded binary file config._pastebinfile = tempfile.TemporaryFile('w+b') - oldwrite = tr.writer.write + oldwrite = tr._tw.write def tee_write(s, **kwargs): oldwrite(s, **kwargs) @@ -33,7 +33,7 @@ def pytest_configure(config): s = s.encode('utf-8') config._pastebinfile.write(s) - tr.writer.write = tee_write + tr._tw.write = tee_write def pytest_unconfigure(config): @@ -45,7 +45,7 @@ def pytest_unconfigure(config): del config._pastebinfile # undo our patching in the terminal reporter tr = config.pluginmanager.getplugin('terminalreporter') - del tr.writer.__dict__['write'] + del tr._tw.__dict__['write'] # write summary tr.write_sep("=", "Sending information to Paste Service") pastebinurl = create_new_paste(sessionlog) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index ab46911e6..a1e5b4380 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -308,9 +308,9 @@ def pytest_terminal_summary(terminalreporter): show_simple(terminalreporter, lines, 'passed', "PASSED %s") if lines: - tr.writer.sep("=", "short test summary info") + tr._tw.sep("=", "short test summary info") for line in lines: - tr.writer.line(line) + tr._tw.line(line) def show_simple(terminalreporter, lines, stat, format): diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 7a18b94a7..8538ee6aa 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -8,7 +8,6 @@ import itertools import platform import sys import time -import warnings import pluggy import py @@ -145,25 +144,15 @@ class TerminalReporter: self.startdir = py.path.local() if file is None: file = sys.stdout - self._writer = _pytest.config.create_terminal_writer(config, file) - self._screen_width = self.writer.fullwidth + self._tw = _pytest.config.create_terminal_writer(config, file) + self._screen_width = self._tw.fullwidth self.currentfspath = None self.reportchars = getreportopt(config) - self.hasmarkup = self.writer.hasmarkup + self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() self._progress_items_reported = 0 self._show_progress_info = self.config.getini('console_output_style') == 'progress' - @property - def writer(self): - return self._writer - - @property - def _tw(self): - warnings.warn(DeprecationWarning('TerminalReporter._tw is deprecated, use TerminalReporter.writer instead'), - stacklevel=2) - return self.writer - def hasopt(self, char): char = {'xfailed': 'x', 'skipped': 's'}.get(char, char) return char in self.reportchars @@ -175,33 +164,33 @@ class TerminalReporter: self._write_progress_information_filling_space() self.currentfspath = fspath fspath = self.startdir.bestrelpath(fspath) - self.writer.line() - self.writer.write(fspath + " ") - self.writer.write(res) + self._tw.line() + self._tw.write(fspath + " ") + self._tw.write(res) def write_ensure_prefix(self, prefix, extra="", **kwargs): if self.currentfspath != prefix: - self.writer.line() + self._tw.line() self.currentfspath = prefix - self.writer.write(prefix) + self._tw.write(prefix) if extra: - self.writer.write(extra, **kwargs) + self._tw.write(extra, **kwargs) self.currentfspath = -2 self._write_progress_information_filling_space() def ensure_newline(self): if self.currentfspath: - self.writer.line() + self._tw.line() self.currentfspath = None def write(self, content, **markup): - self.writer.write(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.writer.line(line, **markup) + self._tw.line(line, **markup) def rewrite(self, line, **markup): """ @@ -214,22 +203,22 @@ class TerminalReporter: """ erase = markup.pop('erase', False) if erase: - fill_count = self.writer.fullwidth - len(line) - 1 + fill_count = self._tw.fullwidth - len(line) - 1 fill = ' ' * fill_count else: fill = '' line = str(line) - self.writer.write("\r" + line + fill, **markup) + self._tw.write("\r" + line + fill, **markup) def write_sep(self, sep, title=None, **markup): self.ensure_newline() - self.writer.sep(sep, title, **markup) + self._tw.sep(sep, title, **markup) def section(self, title, sep="=", **kw): - self.writer.sep(sep, title, **kw) + self._tw.sep(sep, title, **kw) def line(self, msg, **kw): - self.writer.line(msg, **kw) + self._tw.line(msg, **kw) def pytest_internalerror(self, excrepr): for line in six.text_type(excrepr).split("\n"): @@ -282,7 +271,7 @@ class TerminalReporter: if not running_xdist and self.showfspath: self.write_fspath_result(rep.nodeid, letter) else: - self.writer.write(letter) + self._tw.write(letter) self._write_progress_if_past_edge() else: if markup is None: @@ -299,13 +288,13 @@ class TerminalReporter: self.write_ensure_prefix(line, word, **markup) else: self.ensure_newline() - self.writer.write("[%s]" % rep.node.gateway.id) + self._tw.write("[%s]" % rep.node.gateway.id) if self._show_progress_info: - self.writer.write(self._get_progress_information_message() + " ", cyan=True) + self._tw.write(self._get_progress_information_message() + " ", cyan=True) else: - self.writer.write(' ') - self.writer.write(word, **markup) - self.writer.write(" " + line) + self._tw.write(' ') + self._tw.write(word, **markup) + self._tw.write(" " + line) self.currentfspath = -2 def _write_progress_if_past_edge(self): @@ -316,10 +305,10 @@ class TerminalReporter: self._write_progress_information_filling_space() return - past_edge = self.writer.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width + past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width if past_edge: msg = self._get_progress_information_message() - self.writer.write(msg + '\n', cyan=True) + self._tw.write(msg + '\n', cyan=True) _PROGRESS_LENGTH = len(' [100%]') @@ -331,7 +320,7 @@ class TerminalReporter: if not self._show_progress_info: return msg = self._get_progress_information_message() - fill = ' ' * (self.writer.fullwidth - self.writer.chars_on_current_line - len(msg) - 1) + fill = ' ' * (self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1) self.write(fill + msg, cyan=True) def pytest_collection(self): @@ -418,9 +407,9 @@ class TerminalReporter: if self.config.option.collectonly: self._printcollecteditems(session.items) if self.stats.get('failed'): - self.writer.sep("!", "collection failures") + self._tw.sep("!", "collection failures") for rep in self.stats.get('failed'): - rep.toterminal(self.writer) + rep.toterminal(self._tw) return 1 return 0 lines = self.config.hook.pytest_report_collectionfinish( @@ -438,12 +427,12 @@ class TerminalReporter: name = item.nodeid.split('::', 1)[0] counts[name] = counts.get(name, 0) + 1 for name, count in sorted(counts.items()): - self.writer.line("%s: %d" % (name, count)) + self._tw.line("%s: %d" % (name, count)) else: for item in items: nodeid = item.nodeid nodeid = nodeid.replace("::()::", "::") - self.writer.line(nodeid) + self._tw.line(nodeid) return stack = [] indent = "" @@ -458,13 +447,13 @@ class TerminalReporter: # if col.name == "()": # continue indent = (len(stack) - 1) * " " - self.writer.line("%s%s" % (indent, col)) + self._tw.line("%s%s" % (indent, col)) @pytest.hookimpl(hookwrapper=True) def pytest_sessionfinish(self, exitstatus): outcome = yield outcome.get_result() - self.writer.line("") + self._tw.line("") summary_exit_codes = ( EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED) @@ -494,10 +483,10 @@ class TerminalReporter: self.write_sep("!", msg) if "KeyboardInterrupt" in msg: if self.config.option.fulltrace: - excrepr.toterminal(self.writer) + excrepr.toterminal(self._tw) else: - self.writer.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True) - excrepr.reprcrash.toterminal(self.writer) + self._tw.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True) + excrepr.reprcrash.toterminal(self._tw) def _locationline(self, nodeid, fspath, lineno, domain): def mkrel(nodeid): @@ -554,13 +543,13 @@ class TerminalReporter: self.write_sep("=", "warnings summary", yellow=True, bold=False) for location, warning_records in grouped: - self.writer.line(str(location) or '') + self._tw.line(str(location) or '') for w in warning_records: lines = w.message.splitlines() indented = '\n'.join(' ' + x for x in lines) - self.writer.line(indented) - self.writer.line() - self.writer.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html') + self._tw.line(indented) + self._tw.line() + self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html') def summary_passes(self): if self.config.option.tbstyle != "no": @@ -577,10 +566,10 @@ class TerminalReporter: def print_teardown_sections(self, rep): for secname, content in rep.sections: if 'teardown' in secname: - self.writer.sep('-', secname) + self._tw.sep('-', secname) if content[-1:] == "\n": content = content[:-1] - self.writer.line(content) + self._tw.line(content) def summary_failures(self): if self.config.option.tbstyle != "no": @@ -620,12 +609,12 @@ class TerminalReporter: self._outrep_summary(rep) def _outrep_summary(self, rep): - rep.toterminal(self.writer) + rep.toterminal(self._tw) for secname, content in rep.sections: - self.writer.sep("-", secname) + self._tw.sep("-", secname) if content[-1:] == "\n": content = content[:-1] - self.writer.line(content) + self._tw.line(content) def summary_stats(self): session_duration = time.time() - self._sessionstarttime diff --git a/testing/test_mark.py b/testing/test_mark.py index bf50e1587..46bf0b0e7 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -593,7 +593,7 @@ class TestFunctional(object): request.applymarker(pytest.mark.hello) def pytest_terminal_summary(terminalreporter): values = terminalreporter.stats['passed'] - terminalreporter.writer.line("keyword: %s" % values[0].keywords) + terminalreporter._tw.line("keyword: %s" % values[0].keywords) """) testdir.makepyfile(""" def test_func(arg): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 485a3d8c4..98a8ca121 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -219,7 +219,7 @@ class TestTerminal(object): f = py.io.TextIO() monkeypatch.setattr(f, 'isatty', lambda *args: True) tr = TerminalReporter(config, f) - tr.writer.fullwidth = 10 + tr._tw.fullwidth = 10 tr.write('hello') tr.rewrite('hey', erase=True) assert f.getvalue() == 'hello' + '\r' + 'hey' + (6 * ' ') From 95de11a44e9dc1c59263455120814f25072cea2e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 23 Nov 2017 19:34:52 -0200 Subject: [PATCH 156/159] Block pytest-catchlog and issue a warning Trying to install pytest-3.3 and pytest-catchlog will result in an option conflicts because both declare the same options. --- _pytest/config.py | 4 ++++ testing/deprecated_test.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/_pytest/config.py b/_pytest/config.py index 0343951da..6eb2d6c5c 100644 --- a/_pytest/config.py +++ b/_pytest/config.py @@ -242,6 +242,10 @@ class PytestPluginManager(PluginManager): return opts def register(self, plugin, name=None): + if name == 'pytest_catchlog': + self._warn('pytest-catchlog plugin has been merged into the core, ' + 'please remove it from your requirements.') + return ret = super(PytestPluginManager, self).register(plugin, name) if ret: self.hook.pytest_plugin_registered.call_historic( diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 6e1e16331..f3c40cb3d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -99,3 +99,16 @@ def test_metafunc_addcall_deprecated(testdir): "*Metafunc.addcall is deprecated*", "*2 passed, 2 warnings*", ]) + + +def test_pytest_catchlog_deprecated(testdir): + testdir.makepyfile(""" + def test_func(pytestconfig): + pytestconfig.pluginmanager.register(None, 'pytest_catchlog') + """) + res = testdir.runpytest() + assert res.ret == 0 + res.stdout.fnmatch_lines([ + "*pytest-catchlog plugin has been merged into the core*", + "*1 passed, 1 warnings*", + ]) From 0de1a65644ba5c4d3a1ac7d522bc0ae5666f0a49 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 24 Nov 2017 14:42:25 -0200 Subject: [PATCH 157/159] Update pluggy pin to pluggy>=0.5,<0.7 after pluggy-0.6 release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f586101ea..3eb38efe6 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ def main(): # if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy; # used by tox.ini to test with pluggy master if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ: - install_requires.append('pluggy>=0.5,<0.6') + install_requires.append('pluggy>=0.5,<0.7') if has_environment_marker_support(): extras_require[':python_version<"3.0"'] = ['funcsigs'] extras_require[':sys_platform=="win32"'] = ['colorama'] From 6eeacaba3e77c69efde876939376f4d47c8f7514 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 26 Nov 2017 09:00:08 -0200 Subject: [PATCH 158/159] Fix typos in CHANGELOG --- CHANGELOG.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2779761ed..907c952f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,13 @@ Pytest 3.3.0 (2017-11-23) Deprecations and Removals ------------------------- +- Pytest no longer supports Python **2.6** and **3.3**. Those Python versions + are EOL for some time now and incurr maintenance and compatibility costs on + the pytest core team, and following up with the rest of the community we + decided that they will no longer be supported starting on this version. Users + which still require those versions should pin pytest to ``<3.3``. (`#2812 + `_) + - Remove internal ``_preloadplugins()`` function. This removal is part of the ``pytest_namespace()`` hook deprecation. (`#2236 `_) @@ -31,14 +38,6 @@ Deprecations and Removals with the boolean ``Node._skipped_by_mark``. (`#2767 `_) -- Pytest no longer supports Python **2.6** and **3.3**. Those Python versions - are EOL for some time now and incurr maintanance and compatibility costs on - the pytest core team, and following up with the rest of the community we - decided that they will no longer be supported starting on this version. Users - which still require those versions should pin pytest to ``<3.3``. (`#2812 - `_) - - Features -------- @@ -58,7 +57,7 @@ Features previous output style can be set by setting the new ``console_output_style`` to ``classic``. (`#2657 `_) -- Match ``warns`` signature to ``raises`` by adding ``match`` keyworkd. (`#2708 +- Match ``warns`` signature to ``raises`` by adding ``match`` keyword. (`#2708 `_) - Pytest now captures and displays output from the standard `logging` module. @@ -153,7 +152,7 @@ Trivial/Internal Changes - Refactored internal Python 2/3 compatibility code to use ``six``. (`#2642 `_) -- Stop vendoring ``pluggy`` - we're missing out on it's latest changes for not +- Stop vendoring ``pluggy`` - we're missing out on its latest changes for not much benefit (`#2719 `_) - Internal refactor: simplify ascii string escaping by using the From 6cdd85122705393cfcfe08570bae581bf9f2d157 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 27 Nov 2017 10:19:34 -0200 Subject: [PATCH 159/159] CHANGELOG and docs changes requested during review --- CHANGELOG.rst | 16 ++++++++-------- doc/en/backwards-compatibility.rst | 7 +++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 907c952f9..70295eaef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,7 +15,7 @@ Deprecations and Removals ------------------------- - Pytest no longer supports Python **2.6** and **3.3**. Those Python versions - are EOL for some time now and incurr maintenance and compatibility costs on + are EOL for some time now and incur maintenance and compatibility costs on the pytest core team, and following up with the rest of the community we decided that they will no longer be supported starting on this version. Users which still require those versions should pin pytest to ``<3.3``. (`#2812 @@ -30,7 +30,7 @@ Deprecations and Removals ``CallSpec2`` class. (`#2672 `_) -- remove ParameterSet.deprecated_arg_dict - its not a public api and the lack +- Remove ParameterSet.deprecated_arg_dict - its not a public api and the lack of the underscore was a naming error. (`#2675 `_) @@ -46,16 +46,16 @@ Features - Replace the old introspection code in compat.py that determines the available arguments of fixtures with inspect.signature on Python 3 and - funcsigs.signature on Python 2. This should respect __signature__ + funcsigs.signature on Python 2. This should respect ``__signature__`` declarations on functions. (`#2267 `_) -- Report only once tests with global ``pytestmark`` variable. (`#2549 +- Report tests with global ``pytestmark`` variable only once. (`#2549 `_) - Now pytest displays the total progress percentage while running tests. The - previous output style can be set by setting the new ``console_output_style`` - to ``classic``. (`#2657 `_) + previous output style can be set by configuring the ``console_output_style`` + setting to ``classic``. (`#2657 `_) - Match ``warns`` signature to ``raises`` by adding ``match`` keyword. (`#2708 `_) @@ -83,7 +83,7 @@ Features ``err`` can be accessed by attribute. (`#2879 `_) -- Add ``capfdbinary`` a version of ``capfd`` which returns bytes from +- Add ``capfdbinary``, a version of ``capfd`` which returns bytes from ``readouterr()``. (`#2923 `_) @@ -91,7 +91,7 @@ Features ``readouterr()``. (`#2934 `_) -- Implement feature to skip ``setup.py`` files when ran with +- Implement feature to skip ``setup.py`` files when run with ``--doctest-modules``. (`#502 `_) diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst index 2500e85a3..84f2c43ed 100644 --- a/doc/en/backwards-compatibility.rst +++ b/doc/en/backwards-compatibility.rst @@ -96,3 +96,10 @@ Past Releases were never documented and a leftover from a pre-virtualenv era. These entry points also created broken entry points in wheels, so removing them also removes a source of confusion for users. + + + +3.3 +^^^ + +* Dropped support for EOL Python 2.6 and 3.3. \ No newline at end of file