Merge remote-tracking branch 'upstream/features' into malinoff/fix-2124

This commit is contained in:
Bruno Oliveira 2017-11-12 11:16:08 -02:00
commit f074fd9ac6
116 changed files with 2028 additions and 1642 deletions

View File

@ -2,6 +2,3 @@
omit = omit =
# standlonetemplate is read dynamically and tested by test_genscript # standlonetemplate is read dynamically and tested by test_genscript
*standalonetemplate.py *standalonetemplate.py
# oldinterpret could be removed, as it is no longer used in py26+
*oldinterpret.py
vendored_packages

View File

@ -19,20 +19,18 @@ env:
- TOXENV=py27-xdist - TOXENV=py27-xdist
- TOXENV=py27-trial - TOXENV=py27-trial
- TOXENV=py27-numpy - TOXENV=py27-numpy
- TOXENV=py27-pluggymaster
- TOXENV=py36-pexpect - TOXENV=py36-pexpect
- TOXENV=py36-xdist - TOXENV=py36-xdist
- TOXENV=py36-trial - TOXENV=py36-trial
- TOXENV=py36-numpy - TOXENV=py36-numpy
- TOXENV=py36-pluggymaster
- TOXENV=py27-nobyte - TOXENV=py27-nobyte
- TOXENV=doctesting - TOXENV=doctesting
- TOXENV=docs - TOXENV=docs
matrix: matrix:
include: include:
- env: TOXENV=py26
python: '2.6'
- env: TOXENV=py33
python: '3.3'
- env: TOXENV=pypy - env: TOXENV=pypy
python: 'pypy-5.4' python: 'pypy-5.4'
- env: TOXENV=py35 - env: TOXENV=py35

View File

@ -30,6 +30,7 @@ Brianna Laugher
Bruno Oliveira Bruno Oliveira
Cal Leeming Cal Leeming
Carl Friedrich Bolz Carl Friedrich Bolz
Ceridwen
Charles Cloud Charles Cloud
Charnjit SiNGH (CCSJ) Charnjit SiNGH (CCSJ)
Chris Lamb Chris Lamb
@ -65,6 +66,7 @@ Feng Ma
Florian Bruhin Florian Bruhin
Floris Bruynooghe Floris Bruynooghe
Gabriel Reis Gabriel Reis
George Kussumoto
Georgy Dyuldin Georgy Dyuldin
Graham Horler Graham Horler
Greg Price Greg Price
@ -72,6 +74,7 @@ Grig Gheorghiu
Grigorii Eremeev (budulianin) Grigorii Eremeev (budulianin)
Guido Wesdorp Guido Wesdorp
Harald Armin Massa Harald Armin Massa
Hugo van Kemenade
Hui Wang (coldnight) Hui Wang (coldnight)
Ian Bicking Ian Bicking
Jaap Broekhuizen Jaap Broekhuizen
@ -81,6 +84,7 @@ Jason R. Coombs
Javier Domingo Cansino Javier Domingo Cansino
Javier Romero Javier Romero
Jeff Widman Jeff Widman
John Eddie Ayson
John Towler John Towler
Jon Sonesen Jon Sonesen
Jonas Obrist Jonas Obrist
@ -118,6 +122,7 @@ Matt Bachmann
Matt Duck Matt Duck
Matt Williams Matt Williams
Matthias Hafner Matthias Hafner
Maxim Filipenko
mbyt mbyt
Michael Aquilina Michael Aquilina
Michael Birtwell Michael Birtwell
@ -152,6 +157,7 @@ Ronny Pfannschmidt
Ross Lawley Ross Lawley
Russel Winder Russel Winder
Ryan Wooden Ryan Wooden
Samuel Dion-Girardeau
Samuele Pedroni Samuele Pedroni
Segev Finer Segev Finer
Simon Gomizelj Simon Gomizelj
@ -162,9 +168,11 @@ Stefan Zimmermann
Stefano Taschini Stefano Taschini
Steffen Allner Steffen Allner
Stephan Obermann Stephan Obermann
Tarcisio Fischer
Tareq Alayan Tareq Alayan
Ted Xiao Ted Xiao
Thomas Grainger Thomas Grainger
Thomas Hisch
Tom Dalton Tom Dalton
Tom Viner Tom Viner
Trevor Bekolay Trevor Bekolay

View File

@ -76,7 +76,7 @@ Features
- Can run `unittest <http://docs.pytest.org/en/latest/unittest.html>`_ (or trial), - Can run `unittest <http://docs.pytest.org/en/latest/unittest.html>`_ (or trial),
`nose <http://docs.pytest.org/en/latest/nose.html>`_ test suites out of the box; `nose <http://docs.pytest.org/en/latest/nose.html>`_ 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 <http://plugincompat.herokuapp.com>`_ and thriving community; - Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;

View File

@ -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 to find the magic string, so _ARGCOMPLETE env. var is never set, and
this does not need special code. this does not need special code.
argcomplete does not support python 2.5 (although the changes for that
are minor).
Function try_argcomplete(parser) should be called directly before Function try_argcomplete(parser) should be called directly before
the call to ArgumentParser.parse_args(). the call to ArgumentParser.parse_args().

View File

@ -8,8 +8,6 @@ from _pytest.compat import _PY2, _PY3, PY35, safe_str
import py import py
builtin_repr = repr builtin_repr = repr
reprlib = py.builtin._tryimport('repr', 'reprlib')
if _PY3: if _PY3:
from traceback import format_exception_only from traceback import format_exception_only
else: else:
@ -235,7 +233,7 @@ class TracebackEntry(object):
except KeyError: except KeyError:
return False return False
if py.builtin.callable(tbh): if callable(tbh):
return tbh(None if self._excinfo is None else self._excinfo()) return tbh(None if self._excinfo is None else self._excinfo())
else: else:
return tbh return tbh

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import, division, generators, print_function
from bisect import bisect_right from bisect import bisect_right
import sys import sys
import six
import inspect import inspect
import tokenize import tokenize
import py import py
@ -32,7 +33,7 @@ class Source(object):
partlines = part.lines partlines = part.lines
elif isinstance(part, (tuple, list)): elif isinstance(part, (tuple, list)):
partlines = [x.rstrip("\n") for x in part] partlines = [x.rstrip("\n") for x in part]
elif isinstance(part, py.builtin._basestring): elif isinstance(part, six.string_types):
partlines = part.split('\n') partlines = part.split('\n')
if rstrip: if rstrip:
while partlines: while partlines:
@ -341,8 +342,6 @@ def get_statement_startend2(lineno, node):
def getstatementrange_ast(lineno, source, assertion=False, astnode=None): def getstatementrange_ast(lineno, source, assertion=False, astnode=None):
if astnode is None: if astnode is None:
content = str(source) content = str(source)
if sys.version_info < (2, 7):
content += "\n"
try: try:
astnode = compile(content, "source", "exec", 1024) # 1024 for AST astnode = compile(content, "source", "exec", 1024) # 1024 for AST
except ValueError: except ValueError:

View File

@ -1,11 +0,0 @@
"""
imports symbols from vendored "pluggy" if available, otherwise
falls back to importing "pluggy" from the default namespace.
"""
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

View File

@ -2,8 +2,8 @@
support for presenting detailed information in failing assertions. support for presenting detailed information in failing assertions.
""" """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import py
import sys import sys
import six
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.assertion import rewrite from _pytest.assertion import rewrite
@ -67,10 +67,8 @@ class AssertionState:
def install_importhook(config): def install_importhook(config):
"""Try to install the rewrite hook, raise SystemError if it fails.""" """Try to install the rewrite hook, raise SystemError if it fails."""
# Both Jython and CPython 2.6.0 have AST bugs that make the # Jython has an AST bug that make the assertion rewriting hook malfunction.
# assertion rewriting hook malfunction. if (sys.platform.startswith('java')):
if (sys.platform.startswith('java') or
sys.version_info[:3] == (2, 6, 0)):
raise SystemError('rewrite not supported') raise SystemError('rewrite not supported')
config._assertstate = AssertionState(config, 'rewrite') config._assertstate = AssertionState(config, 'rewrite')
@ -126,7 +124,7 @@ def pytest_runtest_setup(item):
if new_expl: if new_expl:
new_expl = truncate.truncate_if_required(new_expl, item) new_expl = truncate.truncate_if_required(new_expl, item)
new_expl = [line.replace("\n", "\\n") for line in new_expl] 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": if item.config.getvalue("assertmode") == "rewrite":
res = res.replace("%", "%%") res = res.replace("%", "%%")
return res return res

View File

@ -8,6 +8,7 @@ import imp
import marshal import marshal
import os import os
import re import re
import six
import struct import struct
import sys import sys
import types import types
@ -33,7 +34,6 @@ else:
PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_EXT = ".py" + (__debug__ and "c" or "o")
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT 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 ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3
if sys.version_info >= (3, 5): if sys.version_info >= (3, 5):
@ -320,10 +320,6 @@ def _rewrite_test(config, fn):
return None, None return None, None
finally: finally:
del state._indecode 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: try:
tree = ast.parse(source) tree = ast.parse(source)
except SyntaxError: except SyntaxError:
@ -405,10 +401,10 @@ def _saferepr(obj):
""" """
repr = py.io.saferepr(obj) repr = py.io.saferepr(obj)
if py.builtin._istext(repr): if isinstance(repr, six.text_type):
t = py.builtin.text t = six.text_type
else: else:
t = py.builtin.bytes t = six.binary_type
return repr.replace(t("\n"), t("\\n")) return repr.replace(t("\n"), t("\\n"))
@ -427,16 +423,16 @@ def _format_assertmsg(obj):
# contains a newline it gets escaped, however if an object has a # contains a newline it gets escaped, however if an object has a
# .__repr__() which contains newlines it does not get escaped. # .__repr__() which contains newlines it does not get escaped.
# However in either case we want to preserve the newline. # 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 s = obj
is_repr = False is_repr = False
else: else:
s = py.io.saferepr(obj) s = py.io.saferepr(obj)
is_repr = True is_repr = True
if py.builtin._istext(s): if isinstance(s, six.text_type):
t = py.builtin.text t = six.text_type
else: else:
t = py.builtin.bytes t = six.binary_type
s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%")) s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%"))
if is_repr: if is_repr:
s = s.replace(t("\\n"), t("\n~")) s = s.replace(t("\\n"), t("\n~"))
@ -444,15 +440,15 @@ def _format_assertmsg(obj):
def _should_repr_global_name(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): def _format_boolop(explanations, is_or):
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
if py.builtin._istext(explanation): if isinstance(explanation, six.text_type):
t = py.builtin.text t = six.text_type
else: else:
t = py.builtin.bytes t = six.binary_type
return explanation.replace(t('%'), t('%%')) return explanation.replace(t('%'), t('%%'))

View File

@ -7,7 +7,7 @@ Current default behaviour is to truncate assertion explanations at
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import os import os
import py import six
DEFAULT_MAX_LINES = 8 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} lines hidden)'.format(truncated_line_count)
msg += ", {0}" .format(USAGE_MSG) msg += ", {0}" .format(USAGE_MSG)
truncated_explanation.extend([ truncated_explanation.extend([
py.builtin._totext(""), six.text_type(""),
py.builtin._totext(msg), six.text_type(msg),
]) ])
return truncated_explanation return truncated_explanation

View File

@ -4,13 +4,14 @@ import pprint
import _pytest._code import _pytest._code
import py import py
import six
try: try:
from collections import Sequence from collections import Sequence
except ImportError: except ImportError:
Sequence = list Sequence = list
u = py.builtin._totext u = six.text_type
# The _reprcompare attribute on the util module is used by the new assertion # The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was # 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 from difflib import ndiff
explanation = [] explanation = []
if isinstance(left, py.builtin.bytes): if isinstance(left, six.binary_type):
left = u(repr(left)[1:-1]).replace(r'\n', '\n') 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') right = u(repr(right)[1:-1]).replace(r'\n', '\n')
if not verbose: if not verbose:
i = 0 # just in case left or right has zero length i = 0 # just in case left or right has zero length

View File

@ -4,6 +4,7 @@ per-test stdout/stderr capturing mechanism.
""" """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import collections
import contextlib import contextlib
import sys import sys
import os import os
@ -11,11 +12,10 @@ import io
from io import UnsupportedOperation from io import UnsupportedOperation
from tempfile import TemporaryFile from tempfile import TemporaryFile
import py import six
import pytest import pytest
from _pytest.compat import CaptureIO from _pytest.compat import CaptureIO
unicode = py.builtin.text
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'} patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
@ -44,7 +44,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
pluginmanager.register(capman, "capturemanager") pluginmanager.register(capman, "capturemanager")
# make sure that capturemanager is properly reset at final shutdown # make sure that capturemanager is properly reset at final shutdown
early_config.add_cleanup(capman.reset_capturings) early_config.add_cleanup(capman.stop_global_capturing)
# make sure logging does not raise exceptions at the end # make sure logging does not raise exceptions at the end
def silence_logging_at_shutdown(): def silence_logging_at_shutdown():
@ -53,17 +53,30 @@ def pytest_load_initial_conftests(early_config, parser, args):
early_config.add_cleanup(silence_logging_at_shutdown) early_config.add_cleanup(silence_logging_at_shutdown)
# finally trigger conftest loading but while capturing (issue93) # finally trigger conftest loading but while capturing (issue93)
capman.init_capturings() capman.start_global_capturing()
outcome = yield outcome = yield
out, err = capman.suspendcapture() out, err = capman.suspend_global_capture()
if outcome.excinfo is not None: if outcome.excinfo is not None:
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)
class CaptureManager: 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): def __init__(self, method):
self._method = method self._method = method
self._global_capturing = None
def _getcapture(self, method): def _getcapture(self, method):
if method == "fd": if method == "fd":
@ -75,23 +88,24 @@ class CaptureManager:
else: else:
raise ValueError("unknown capturing method: %r" % method) raise ValueError("unknown capturing method: %r" % method)
def init_capturings(self): def start_global_capturing(self):
assert not hasattr(self, "_capturing") assert self._global_capturing is None
self._capturing = self._getcapture(self._method) self._global_capturing = self._getcapture(self._method)
self._capturing.start_capturing() self._global_capturing.start_capturing()
def reset_capturings(self): def stop_global_capturing(self):
cap = self.__dict__.pop("_capturing", None) if self._global_capturing is not None:
if cap is not None: self._global_capturing.pop_outerr_to_orig()
cap.pop_outerr_to_orig() self._global_capturing.stop_capturing()
cap.stop_capturing() self._global_capturing = None
def resumecapture(self): def resume_global_capture(self):
self._capturing.resume_capturing() self._global_capturing.resume_capturing()
def suspendcapture(self, in_=False): def suspend_global_capture(self, item=None, in_=False):
self.deactivate_funcargs() if item is not None:
cap = getattr(self, "_capturing", None) self.deactivate_fixture(item)
cap = getattr(self, "_global_capturing", None)
if cap is not None: if cap is not None:
try: try:
outerr = cap.readouterr() outerr = cap.readouterr()
@ -99,23 +113,26 @@ class CaptureManager:
cap.suspend_capturing(in_=in_) cap.suspend_capturing(in_=in_)
return outerr return outerr
def activate_funcargs(self, pyfuncitem): def activate_fixture(self, item):
capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None) """If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
if capfuncarg is not None: the global capture.
capfuncarg._start() """
self._capfuncarg = capfuncarg fixture = getattr(item, "_capture_fixture", None)
if fixture is not None:
fixture._start()
def deactivate_funcargs(self): def deactivate_fixture(self, item):
capfuncarg = self.__dict__.pop("_capfuncarg", None) """Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
if capfuncarg is not None: fixture = getattr(item, "_capture_fixture", None)
capfuncarg.close() if fixture is not None:
fixture.close()
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector): def pytest_make_collect_report(self, collector):
if isinstance(collector, pytest.File): if isinstance(collector, pytest.File):
self.resumecapture() self.resume_global_capture()
outcome = yield outcome = yield
out, err = self.suspendcapture() out, err = self.suspend_global_capture()
rep = outcome.get_result() rep = outcome.get_result()
if out: if out:
rep.sections.append(("Captured stdout", out)) rep.sections.append(("Captured stdout", out))
@ -126,34 +143,39 @@ class CaptureManager:
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item): 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 yield
self.suspendcapture_item(item, "setup") self.suspend_capture_item(item, "setup")
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item): def pytest_runtest_call(self, item):
self.resumecapture() self.resume_global_capture()
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 yield
# self.deactivate_funcargs() called from suspendcapture() self.suspend_capture_item(item, "call")
self.suspendcapture_item(item, "call")
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item): def pytest_runtest_teardown(self, item):
self.resumecapture() self.resume_global_capture()
self.activate_fixture(item)
yield yield
self.suspendcapture_item(item, "teardown") self.suspend_capture_item(item, "teardown")
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self, excinfo): def pytest_keyboard_interrupt(self, excinfo):
self.reset_capturings() self.stop_global_capturing()
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_internalerror(self, excinfo): def pytest_internalerror(self, excinfo):
self.reset_capturings() self.stop_global_capturing()
def suspendcapture_item(self, item, when, in_=False): def suspend_capture_item(self, item, when, in_=False):
out, err = self.suspendcapture(in_=in_) out, err = self.suspend_global_capture(item, in_=in_)
item.add_report_section(when, "stdout", out) item.add_report_section(when, "stdout", out)
item.add_report_section(when, "stderr", err) item.add_report_section(when, "stderr", err)
@ -169,8 +191,8 @@ def capsys(request):
""" """
if "capfd" in request.fixturenames: if "capfd" in request.fixturenames:
raise request.raiseerror(error_capsysfderror) raise request.raiseerror(error_capsysfderror)
request.node._capfuncarg = c = CaptureFixture(SysCapture, request) with _install_capture_fixture_on_item(request, SysCapture) as fixture:
return c yield fixture
@pytest.fixture @pytest.fixture
@ -182,9 +204,29 @@ def capfd(request):
if "capsys" in request.fixturenames: if "capsys" in request.fixturenames:
request.raiseerror(error_capsysfderror) request.raiseerror(error_capsysfderror)
if not hasattr(os, 'dup'): if not hasattr(os, 'dup'):
pytest.skip("capfd funcarg needs os.dup") pytest.skip("capfd fixture needs os.dup function which is not available in this system")
request.node._capfuncarg = c = CaptureFixture(FDCapture, request) with _install_capture_fixture_on_item(request, FDCapture) as fixture:
return c 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: class CaptureFixture:
@ -211,12 +253,14 @@ class CaptureFixture:
@contextlib.contextmanager @contextlib.contextmanager
def disabled(self): def disabled(self):
self._capture.suspend_capturing()
capmanager = self.request.config.pluginmanager.getplugin('capturemanager') capmanager = self.request.config.pluginmanager.getplugin('capturemanager')
capmanager.suspendcapture_item(self.request.node, "call", in_=True) capmanager.suspend_global_capture(item=None, in_=False)
try: try:
yield yield
finally: finally:
capmanager.resumecapture() capmanager.resume_global_capture()
self._capture.resume_capturing()
def safe_text_dupfile(f, mode, default_encoding="UTF8"): def safe_text_dupfile(f, mode, default_encoding="UTF8"):
@ -246,7 +290,7 @@ class EncodedFile(object):
self.encoding = encoding self.encoding = encoding
def write(self, obj): def write(self, obj):
if isinstance(obj, unicode): if isinstance(obj, six.text_type):
obj = obj.encode(self.encoding, "replace") obj = obj.encode(self.encoding, "replace")
self.buffer.write(obj) self.buffer.write(obj)
@ -263,6 +307,9 @@ class EncodedFile(object):
return getattr(object.__getattribute__(self, "buffer"), name) return getattr(object.__getattribute__(self, "buffer"), name)
CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
class MultiCapture(object): class MultiCapture(object):
out = err = in_ = None out = err = in_ = None
@ -323,7 +370,7 @@ class MultiCapture(object):
def readouterr(self): def readouterr(self):
""" return snapshot unicode value of stdout/stderr capturings. """ """ return snapshot unicode value of stdout/stderr capturings. """
return (self.out.snap() if self.out 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 "") self.err.snap() if self.err is not None else "")
@ -377,7 +424,7 @@ class FDCapture:
if res: if res:
enc = getattr(f, "encoding", None) enc = getattr(f, "encoding", None)
if enc and isinstance(res, bytes): if enc and isinstance(res, bytes):
res = py.builtin._totext(res, enc, "replace") res = six.text_type(res, enc, "replace")
f.truncate(0) f.truncate(0)
f.seek(0) f.seek(0)
return res return res
@ -402,7 +449,7 @@ class FDCapture:
def writeorg(self, data): def writeorg(self, data):
""" write to original file descriptor. """ """ 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 data = data.encode("utf8") # XXX use encoding of original stream
os.write(self.targetfd_save, data) os.write(self.targetfd_save, data)

View File

@ -2,18 +2,18 @@
python version compatibility code python version compatibility code
""" """
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys
import inspect import codecs
import types
import re
import functools import functools
import inspect
import re
import sys
import py import py
import _pytest import _pytest
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
try: try:
import enum import enum
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
@ -25,6 +25,12 @@ _PY3 = sys.version_info > (3, 0)
_PY2 = not _PY3 _PY2 = not _PY3
if _PY3:
from inspect import signature, Parameter as Parameter
else:
from funcsigs import signature, Parameter as Parameter
NoneType = type(None) NoneType = type(None)
NOTSET = object() NOTSET = object()
@ -32,12 +38,10 @@ PY35 = sys.version_info[:2] >= (3, 5)
PY36 = sys.version_info[:2] >= (3, 6) PY36 = sys.version_info[:2] >= (3, 6)
MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError' MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError'
if hasattr(inspect, 'signature'):
def _format_args(func): def _format_args(func):
return str(inspect.signature(func)) return str(signature(func))
else:
def _format_args(func):
return inspect.formatargspec(*inspect.getargspec(func))
isfunction = inspect.isfunction isfunction = inspect.isfunction
isclass = inspect.isclass isclass = inspect.isclass
@ -63,7 +67,6 @@ def iscoroutinefunction(func):
def getlocation(function, curdir): def getlocation(function, curdir):
import inspect
fn = py.path.local(inspect.getfile(function)) fn = py.path.local(inspect.getfile(function))
lineno = py.builtin._getcode(function).co_firstlineno lineno = py.builtin._getcode(function).co_firstlineno
if fn.relto(curdir): if fn.relto(curdir):
@ -83,60 +86,64 @@ def num_mock_patch_args(function):
return len(patchings) 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.
@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:])
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.
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)) # 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: if _PY3:
import codecs
imap = map
izip = zip
STRING_TYPES = bytes, str STRING_TYPES = bytes, str
UNICODE_TYPES = str, UNICODE_TYPES = str,
def _ascii_escaped(val): 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 """If val is pure ascii, returns it as a str(). Otherwise, escapes
bytes objects into a sequence of escaped bytes: bytes objects into a sequence of escaped bytes:
@ -155,22 +162,14 @@ if _PY3:
""" """
if isinstance(val, bytes): if isinstance(val, bytes):
if val: return _bytes_to_ascii(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 ''
else: else:
return val.encode('unicode_escape').decode('ascii') return val.encode('unicode_escape').decode('ascii')
else: else:
STRING_TYPES = bytes, str, unicode STRING_TYPES = bytes, str, unicode
UNICODE_TYPES = unicode, UNICODE_TYPES = unicode,
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 """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, object, return it unchanged if it is a full ascii string,
otherwise escape it into its binary form. otherwise escape it into its binary form.
@ -222,9 +221,6 @@ def getfslineno(obj):
def getimfunc(func): def getimfunc(func):
try: try:
return func.__func__ return func.__func__
except AttributeError:
try:
return func.im_func
except AttributeError: except AttributeError:
return func return func

View File

@ -6,6 +6,7 @@ import traceback
import types import types
import warnings import warnings
import six
import py import py
# DON't import pytest here because it causes import cycle troubles # DON't import pytest here because it causes import cycle troubles
import sys import sys
@ -13,7 +14,7 @@ import os
import _pytest._code import _pytest._code
import _pytest.hookspec # the extension point definitions import _pytest.hookspec # the extension point definitions
import _pytest.assertion import _pytest.assertion
from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker from pluggy import PluginManager, HookimplMarker, HookspecMarker
from _pytest.compat import safe_str from _pytest.compat import safe_str
hookimpl = HookimplMarker("pytest") hookimpl = HookimplMarker("pytest")
@ -100,27 +101,18 @@ def directory_arg(path, optname):
return path return path
_preinit = []
default_plugins = ( default_plugins = (
"mark main terminal runner python fixtures debugging unittest capture skipping " "mark main terminal runner python fixtures debugging unittest capture skipping "
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion "
"junitxml resultlog doctest cacheprovider freeze_support " "junitxml resultlog doctest cacheprovider freeze_support "
"setuponly setupplan warnings").split() "setuponly setupplan warnings logging").split()
builtin_plugins = set(default_plugins) builtin_plugins = set(default_plugins)
builtin_plugins.add("pytester") builtin_plugins.add("pytester")
def _preloadplugins():
assert not _preinit
_preinit.append(get_config())
def get_config(): def get_config():
if _preinit:
return _preinit.pop(0)
# subsequent calls to main will create a fresh instance # subsequent calls to main will create a fresh instance
pluginmanager = PytestPluginManager() pluginmanager = PytestPluginManager()
config = Config(pluginmanager) config = Config(pluginmanager)
@ -158,7 +150,7 @@ def _prepareconfig(args=None, plugins=None):
try: try:
if plugins: if plugins:
for plugin in plugins: for plugin in plugins:
if isinstance(plugin, py.builtin._basestring): if isinstance(plugin, six.string_types):
pluginmanager.consider_pluginarg(plugin) pluginmanager.consider_pluginarg(plugin)
else: else:
pluginmanager.register(plugin) pluginmanager.register(plugin)
@ -173,7 +165,7 @@ def _prepareconfig(args=None, plugins=None):
class PytestPluginManager(PluginManager): class PytestPluginManager(PluginManager):
""" """
Overwrites :py:class:`pluggy.PluginManager <_pytest.vendored_packages.pluggy.PluginManager>` to add pytest-specific Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add pytest-specific
functionality: functionality:
* loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and * loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and
@ -211,7 +203,7 @@ class PytestPluginManager(PluginManager):
""" """
.. deprecated:: 2.8 .. deprecated:: 2.8
Use :py:meth:`pluggy.PluginManager.add_hookspecs <_pytest.vendored_packages.pluggy.PluginManager.add_hookspecs>` Use :py:meth:`pluggy.PluginManager.add_hookspecs <PluginManager.add_hookspecs>`
instead. instead.
""" """
warning = dict(code="I2", warning = dict(code="I2",
@ -249,17 +241,6 @@ class PytestPluginManager(PluginManager):
"historic": hasattr(method, "historic")} "historic": hasattr(method, "historic")}
return opts 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): def register(self, plugin, name=None):
ret = super(PytestPluginManager, self).register(plugin, name) ret = super(PytestPluginManager, self).register(plugin, name)
if ret: if ret:
@ -430,7 +411,7 @@ class PytestPluginManager(PluginManager):
# "terminal" or "capture". Those plugins are registered under their # "terminal" or "capture". Those plugins are registered under their
# basename for historic purposes but must be imported with the # basename for historic purposes but must be imported with the
# _pytest prefix. # _pytest prefix.
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) modname = str(modname)
if self.get_plugin(modname) is not None: if self.get_plugin(modname) is not None:
return return
@ -442,12 +423,12 @@ class PytestPluginManager(PluginManager):
try: try:
__import__(importspec) __import__(importspec)
except ImportError as e: except ImportError as e:
new_exc = ImportError('Error importing plugin "%s": %s' % (modname, safe_str(e.args[0]))) new_exc_type = ImportError
# copy over name and path attributes new_exc_message = 'Error importing plugin "%s": %s' % (modname, safe_str(e.args[0]))
for attr in ('name', 'path'): new_exc = new_exc_type(new_exc_message)
if hasattr(e, attr):
setattr(new_exc, attr, getattr(e, attr)) six.reraise(new_exc_type, new_exc, sys.exc_info()[2])
raise new_exc
except Exception as e: except Exception as e:
import pytest import pytest
if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception): if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception):
@ -643,7 +624,7 @@ class Argument:
pass pass
else: else:
# this might raise a keyerror as well, don't want to catch that # this might raise a keyerror as well, don't want to catch that
if isinstance(typ, py.builtin._basestring): if isinstance(typ, six.string_types):
if typ == 'choice': if typ == 'choice':
warnings.warn( warnings.warn(
'type argument to addoption() is a string %r.' 'type argument to addoption() is a string %r.'
@ -950,7 +931,7 @@ class Config(object):
fslocation=fslocation, nodeid=nodeid)) fslocation=fslocation, nodeid=nodeid))
def get_terminal_writer(self): 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): def pytest_cmdline_parse(self, pluginmanager, args):
# REF1 assert self == pluginmanager.config, (self, pluginmanager.config) # REF1 assert self == pluginmanager.config, (self, pluginmanager.config)
@ -968,7 +949,7 @@ class Config(object):
) )
res = self.hook.pytest_internalerror(excrepr=excrepr, res = self.hook.pytest_internalerror(excrepr=excrepr,
excinfo=excinfo) excinfo=excinfo)
if not py.builtin.any(res): if not any(res):
for line in str(excrepr).split("\n"): for line in str(excrepr).split("\n"):
sys.stderr.write("INTERNALERROR> %s\n" % line) sys.stderr.write("INTERNALERROR> %s\n" % line)
sys.stderr.flush() sys.stderr.flush()
@ -1074,9 +1055,10 @@ class Config(object):
"(are you using python -O?)\n") "(are you using python -O?)\n")
def _preparse(self, args, addopts=True): def _preparse(self, args, addopts=True):
self._initini(args)
if addopts: if addopts:
args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args
self._initini(args)
if addopts:
args[:] = self.getini("addopts") + args args[:] = self.getini("addopts") + args
self._checkversion() self._checkversion()
self._consider_importhook(args) self._consider_importhook(args)

View File

@ -54,7 +54,7 @@ class pytestPDB:
if cls._pluginmanager is not None: if cls._pluginmanager is not None:
capman = cls._pluginmanager.getplugin("capturemanager") capman = cls._pluginmanager.getplugin("capturemanager")
if capman: if capman:
capman.suspendcapture(in_=True) capman.suspend_global_capture(in_=True)
tw = _pytest.config.create_terminal_writer(cls._config) tw = _pytest.config.create_terminal_writer(cls._config)
tw.line() tw.line()
tw.sep(">", "PDB set_trace (IO-capturing turned off)") tw.sep(">", "PDB set_trace (IO-capturing turned off)")
@ -66,7 +66,7 @@ class PdbInvoke:
def pytest_exception_interact(self, node, call, report): def pytest_exception_interact(self, node, call, report):
capman = node.config.pluginmanager.getplugin("capturemanager") capman = node.config.pluginmanager.getplugin("capturemanager")
if capman: if capman:
out, err = capman.suspendcapture(in_=True) out, err = capman.suspend_global_capture(in_=True)
sys.stdout.write(out) sys.stdout.write(out)
sys.stdout.write(err) sys.stdout.write(err)
_enter_pdb(node, call.excinfo, report) _enter_pdb(node, call.excinfo, report)
@ -83,7 +83,7 @@ def _enter_pdb(node, excinfo, rep):
# XXX we re-use the TerminalReporter's terminalwriter # XXX we re-use the TerminalReporter's terminalwriter
# because this seems to avoid some encoding related troubles # because this seems to avoid some encoding related troubles
# for not completely clear reasons. # for not completely clear reasons.
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw tw = node.config.pluginmanager.getplugin("terminalreporter").writer
tw.line() tw.line()
tw.sep(">", "traceback") tw.sep(">", "traceback")
rep.toterminal(tw) rep.toterminal(tw)

View File

@ -40,3 +40,8 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
" please use pytest.param(..., marks=...) instead.\n" " please use pytest.param(..., marks=...) instead.\n"
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html" "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"
)

View File

@ -50,12 +50,19 @@ def pytest_addoption(parser):
def pytest_collect_file(path, parent): def pytest_collect_file(path, parent):
config = parent.config config = parent.config
if path.ext == ".py": if path.ext == ".py":
if config.option.doctestmodules: if config.option.doctestmodules and not _is_setup_py(config, path, parent):
return DoctestModule(path, parent) return DoctestModule(path, parent)
elif _is_doctest(config, path, parent): elif _is_doctest(config, path, parent):
return DoctestTextfile(path, parent) return DoctestTextfile(path, parent)
def _is_setup_py(config, path, parent):
if path.basename != "setup.py":
return False
contents = path.read()
return 'setuptools' in contents or 'distutils' in contents
def _is_doctest(config, path, parent): def _is_doctest(config, path, parent):
if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path): if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path):
return True return True

View File

@ -8,6 +8,7 @@ import functools
import py import py
from py._code.code import FormattedExcinfo from py._code.code import FormattedExcinfo
import attr
import _pytest import _pytest
from _pytest import nodes from _pytest import nodes
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
@ -22,18 +23,17 @@ from _pytest.compat import (
from _pytest.outcomes import fail, TEST_OUTCOME from _pytest.outcomes import fail, TEST_OUTCOME
if sys.version_info[:2] == (2, 6):
from ordereddict import OrderedDict
else:
from collections import OrderedDict from collections import OrderedDict
def pytest_sessionstart(session): def pytest_sessionstart(session):
import _pytest.python import _pytest.python
scopename2class.update({ scopename2class.update({
'class': _pytest.python.Class, 'class': _pytest.python.Class,
'module': _pytest.python.Module, 'module': _pytest.python.Module,
'function': _pytest.main.Item, 'function': _pytest.main.Item,
'session': _pytest.main.Session,
}) })
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
@ -65,8 +65,6 @@ def scopeproperty(name=None, doc=None):
def get_scope_node(node, scope): def get_scope_node(node, scope):
cls = scopename2class.get(scope) cls = scopename2class.get(scope)
if cls is None: if cls is None:
if scope == "session":
return node.session
raise ValueError("unknown scope") raise ValueError("unknown scope")
return node.getparent(cls) return node.getparent(cls)
@ -736,8 +734,7 @@ class FixtureDef:
where=baseid where=baseid
) )
self.params = params self.params = params
startindex = unittest and 1 or None self.argnames = getfuncargnames(func, is_method=unittest)
self.argnames = getfuncargnames(func, startindex=startindex)
self.unittest = unittest self.unittest = unittest
self.ids = ids self.ids = ids
self._finalizer = [] self._finalizer = []
@ -831,13 +828,21 @@ def pytest_fixture_setup(fixturedef, request):
return result return result
class FixtureFunctionMarker: def _ensure_immutable_ids(ids):
def __init__(self, scope, params, autouse=False, ids=None, name=None): if ids is None:
self.scope = scope return
self.params = params if callable(ids):
self.autouse = autouse return ids
self.ids = ids return tuple(ids)
self.name = name
@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): def __call__(self, function):
if isclass(function): if isclass(function):

View File

@ -107,7 +107,7 @@ def pytest_cmdline_main(config):
def showhelp(config): def showhelp(config):
reporter = config.pluginmanager.get_plugin('terminalreporter') reporter = config.pluginmanager.get_plugin('terminalreporter')
tw = reporter._tw tw = reporter.writer
tw.write(config._parser.optparser.format_help()) tw.write(config._parser.optparser.format_help())
tw.line() tw.line()
tw.line() tw.line()

View File

@ -1,6 +1,6 @@
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
from _pytest._pluggy import HookspecMarker from pluggy import HookspecMarker
hookspec = HookspecMarker("pytest") hookspec = HookspecMarker("pytest")

337
_pytest/logging.py Normal file
View File

@ -0,0 +1,337 @@
from __future__ import absolute_import, division, print_function
import logging
from contextlib import closing, contextmanager
import sys
import six
import pytest
import py
DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
def get_option_ini(config, *names):
for name in names:
ret = config.getoption(name) # 'default' arg won't work as expected
if ret is None:
ret = config.getini(name)
if ret:
return ret
def pytest_addoption(parser):
"""Add options to control log capturing."""
group = parser.getgroup('logging')
def add_option_ini(option, dest, default=None, type=None, **kwargs):
parser.addini(dest, default=default, type=type,
help='default value for ' + option)
group.addoption(option, dest=dest, **kwargs)
add_option_ini(
'--no-print-logs',
dest='log_print', action='store_const', const=False, default=True,
type='bool',
help='disable printing caught logs on failed tests.')
add_option_ini(
'--log-level',
dest='log_level', default=None,
help='logging level used by the logging module')
add_option_ini(
'--log-format',
dest='log_format', default=DEFAULT_LOG_FORMAT,
help='log format as used by the logging module.')
add_option_ini(
'--log-date-format',
dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT,
help='log date format as used by the logging module.')
add_option_ini(
'--log-cli-level',
dest='log_cli_level', default=None,
help='cli logging level.')
add_option_ini(
'--log-cli-format',
dest='log_cli_format', default=None,
help='log format as used by the logging module.')
add_option_ini(
'--log-cli-date-format',
dest='log_cli_date_format', default=None,
help='log date format as used by the logging module.')
add_option_ini(
'--log-file',
dest='log_file', default=None,
help='path to a file when logging will be written to.')
add_option_ini(
'--log-file-level',
dest='log_file_level', default=None,
help='log file logging level.')
add_option_ini(
'--log-file-format',
dest='log_file_format', default=DEFAULT_LOG_FORMAT,
help='log format as used by the logging module.')
add_option_ini(
'--log-file-date-format',
dest='log_file_date_format', default=DEFAULT_LOG_DATE_FORMAT,
help='log date format as used by the logging module.')
@contextmanager
def logging_using_handler(handler, logger=None):
"""Context manager that safely registers a given handler."""
logger = logger or logging.getLogger(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, formatter=None,
level=logging.NOTSET, logger=None):
"""Context manager that prepares the whole logging machinery properly."""
logger = logger or logging.getLogger(logger)
if formatter is not None:
handler.setFormatter(formatter)
handler.setLevel(level)
with logging_using_handler(handler, logger):
orig_level = logger.level
logger.setLevel(min(orig_level, level))
try:
yield handler
finally:
logger.setLevel(orig_level)
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."""
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.
"""
if logger is None:
logger = self.handler
else:
logger = logging.getLogger(logger)
logger.setLevel(level)
@contextmanager
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.
"""
if logger is None:
logger = self.handler
else:
logger = logging.getLogger(logger)
orig_level = logger.level
logger.setLevel(level)
try:
yield
finally:
logger.setLevel(orig_level)
@pytest.fixture
def caplog(request):
"""Access and control log capturing.
Captured logs are available through the following 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 LogCaptureFixture(request.node)
def get_actual_log_level(config, *setting_names):
"""Return the actual logging level."""
for setting_name in setting_names:
log_level = config.getoption(setting_name)
if log_level is None:
log_level = config.getini(setting_name)
if log_level:
break
else:
return
if isinstance(log_level, six.string_types):
log_level = log_level.upper()
try:
return int(getattr(logging, log_level, log_level))
except ValueError:
# Python logging does not recognise this as a logging level
raise pytest.UsageError(
"'{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):
config.pluginmanager.register(LoggingPlugin(config),
'logging-plugin')
class LoggingPlugin(object):
"""Attaches to the logging module and captures log messages for each test.
"""
def __init__(self, config):
"""Creates a new plugin to capture log messages.
The formatter can be safely shared across all handlers so
create a single one for the entire test session here.
"""
self.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(
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(
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)
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')
if log_file:
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', '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
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.hookimpl(hookwrapper=True)
def pytest_runtest_setup(self, item):
with self._runtest_for(item, 'setup'):
yield
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item):
with self._runtest_for(item, 'call'):
yield
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_teardown(self, item):
with self._runtest_for(item, 'teardown'):
yield
@pytest.hookimpl(hookwrapper=True)
def pytest_runtestloop(self, session):
"""Runs all collected test items."""
with self.live_logs:
if self.log_file_handler is not None:
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

View File

@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function
import functools import functools
import os import os
import six
import sys import sys
import _pytest import _pytest
@ -84,15 +85,6 @@ def pytest_addoption(parser):
help="base temporary directory for this test run.") 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): def pytest_configure(config):
__import__('pytest').config = config # compatibiltiy __import__('pytest').config = config # compatibiltiy
@ -111,6 +103,8 @@ def wrap_session(config, doit):
session.exitstatus = doit(config, session) or 0 session.exitstatus = doit(config, session) or 0
except UsageError: except UsageError:
raise raise
except Failed:
session.exitstatus = EXIT_TESTSFAILED
except KeyboardInterrupt: except KeyboardInterrupt:
excinfo = _pytest._code.ExceptionInfo() excinfo = _pytest._code.ExceptionInfo()
if initstate < 2 and isinstance(excinfo.value, exit.Exception): if initstate < 2 and isinstance(excinfo.value, exit.Exception):
@ -168,6 +162,8 @@ def pytest_runtestloop(session):
for i, item in enumerate(session.items): for i, item in enumerate(session.items):
nextitem = session.items[i + 1] if i + 1 < len(session.items) else None nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem) item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
if session.shouldfail:
raise session.Failed(session.shouldfail)
if session.shouldstop: if session.shouldstop:
raise session.Interrupted(session.shouldstop) raise session.Interrupted(session.shouldstop)
return True return True
@ -364,24 +360,6 @@ class Node(object):
def teardown(self): def teardown(self):
pass 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: # noqa
failure = sys.exc_info()
setattr(self, exattrname, failure)
raise
setattr(self, attrname, res)
return res
def listchain(self): def listchain(self):
""" return list of all parent collectors up to self, """ return list of all parent collectors up to self,
starting from root of collection tree. """ starting from root of collection tree. """
@ -399,7 +377,7 @@ class Node(object):
``marker`` can be a string or pytest.mark.* instance. ``marker`` can be a string or pytest.mark.* instance.
""" """
from _pytest.mark import MarkDecorator, MARK_GEN from _pytest.mark import MarkDecorator, MARK_GEN
if isinstance(marker, py.builtin._basestring): if isinstance(marker, six.string_types):
marker = getattr(MARK_GEN, marker) marker = getattr(MARK_GEN, marker)
elif not isinstance(marker, MarkDecorator): elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker") raise ValueError("is not a string or pytest.mark.* Marker")
@ -599,8 +577,13 @@ class Interrupted(KeyboardInterrupt):
__module__ = 'builtins' # for py3 __module__ = 'builtins' # for py3
class Failed(Exception):
""" signals an stop as failed test run. """
class Session(FSCollector): class Session(FSCollector):
Interrupted = Interrupted Interrupted = Interrupted
Failed = Failed
def __init__(self, config): def __init__(self, config):
FSCollector.__init__(self, config.rootdir, parent=None, FSCollector.__init__(self, config.rootdir, parent=None,
@ -608,6 +591,7 @@ class Session(FSCollector):
self.testsfailed = 0 self.testsfailed = 0
self.testscollected = 0 self.testscollected = 0
self.shouldstop = False self.shouldstop = False
self.shouldfail = False
self.trace = config.trace.root.get("collection") self.trace = config.trace.root.get("collection")
self._norecursepatterns = config.getini("norecursedirs") self._norecursepatterns = config.getini("norecursedirs")
self.startdir = py.path.local() self.startdir = py.path.local()
@ -618,6 +602,8 @@ class Session(FSCollector):
@hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
def pytest_collectstart(self): def pytest_collectstart(self):
if self.shouldfail:
raise self.Failed(self.shouldfail)
if self.shouldstop: if self.shouldstop:
raise self.Interrupted(self.shouldstop) raise self.Interrupted(self.shouldstop)
@ -627,7 +613,7 @@ class Session(FSCollector):
self.testsfailed += 1 self.testsfailed += 1
maxfail = self.config.getvalue("maxfail") maxfail = self.config.getvalue("maxfail")
if maxfail and self.testsfailed >= maxfail: if maxfail and self.testsfailed >= maxfail:
self.shouldstop = "stopping after %d failures" % ( self.shouldfail = "stopping after %d failures" % (
self.testsfailed) self.testsfailed)
pytest_collectreport = pytest_runtest_logreport pytest_collectreport = pytest_runtest_logreport

View File

@ -3,10 +3,12 @@ from __future__ import absolute_import, division, print_function
import inspect import inspect
import warnings import warnings
import attr
from collections import namedtuple from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from .compat import imap from six.moves import map
from .deprecated import MARK_PARAMETERSET_UNPACKING from .deprecated import MARK_PARAMETERSET_UNPACKING
from .compat import NOTSET, getfslineno
def alias(name, warning=None): def alias(name, warning=None):
@ -67,9 +69,29 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
return cls(argval, marks=newmarks, id=None) return cls(argval, marks=newmarks, id=None)
@property @classmethod
def deprecated_arg_dict(self): def _for_parameterize(cls, argnames, argvalues, function):
return dict((mark.name, mark) for mark in self.marks) 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): class MarkerError(Exception):
@ -164,22 +186,26 @@ def pytest_collection_modifyitems(items, config):
items[:] = remaining items[:] = remaining
class MarkMapping: @attr.s
class MarkMapping(object):
"""Provides a local mapping for markers where item access """Provides a local mapping for markers where item access
resolves to True if the marker is present. """ resolves to True if the marker is present. """
def __init__(self, keywords): own_mark_names = attr.ib()
mymarks = set()
@classmethod
def from_keywords(cls, keywords):
mark_names = set()
for key, value in keywords.items(): for key, value in keywords.items():
if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator): if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator):
mymarks.add(key) mark_names.add(key)
self._mymarks = mymarks return cls(mark_names)
def __getitem__(self, name): 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. """Provides a local mapping for keywords.
Given a list of names, map any substring of one of these names to True. Given a list of names, map any substring of one of these names to True.
""" """
@ -196,7 +222,7 @@ class KeywordMapping:
def matchmark(colitem, markexpr): def matchmark(colitem, markexpr):
"""Tries to match on any marker names, attached to the given colitem.""" """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): def matchkeyword(colitem, keywordexpr):
@ -285,7 +311,21 @@ def istestfunc(func):
getattr(func, "__name__", "<lambda>") != "<lambda>" getattr(func, "__name__", "<lambda>") != "<lambda>"
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 """ A decorator for test functions and test classes. When applied
it will create :class:`MarkInfo` objects which may be it will create :class:`MarkInfo` objects which may be
:ref:`retrieved by hooks as item keywords <excontrolskip>`. :ref:`retrieved by hooks as item keywords <excontrolskip>`.
@ -319,9 +359,7 @@ class MarkDecorator:
""" """
def __init__(self, mark): mark = attr.ib(validator=attr.validators.instance_of(Mark))
assert isinstance(mark, Mark), repr(mark)
self.mark = mark
name = alias('mark.name') name = alias('mark.name')
args = alias('mark.args') args = alias('mark.args')
@ -401,15 +439,6 @@ def store_legacy_markinfo(func, mark):
holder.add_mark(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): class MarkInfo(object):
""" Marking object created by :class:`MarkDecorator` instances. """ """ Marking object created by :class:`MarkDecorator` instances. """
@ -432,7 +461,7 @@ class MarkInfo(object):
def __iter__(self): def __iter__(self):
""" yield MarkInfo objects each relating to a marking-call. """ """ yield MarkInfo objects each relating to a marking-call. """
return imap(MarkInfo, self._marks) return map(MarkInfo, self._marks)
MARK_GEN = MarkGenerator() MARK_GEN = MarkGenerator()

View File

@ -4,8 +4,7 @@ from __future__ import absolute_import, division, print_function
import os import os
import sys import sys
import re import re
import six
from py.builtin import _basestring
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$") 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): 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" % raise TypeError("must be absolute import path string, not %r" %
(import_path,)) (import_path,))
module, attr = import_path.rsplit('.', 1) module, attr = import_path.rsplit('.', 1)
@ -125,7 +124,7 @@ class MonkeyPatch:
import inspect import inspect
if value is notset: if value is notset:
if not isinstance(target, _basestring): if not isinstance(target, six.string_types):
raise TypeError("use setattr(target, name, value) or " raise TypeError("use setattr(target, name, value) or "
"setattr(target, value) with target being a dotted " "setattr(target, value) with target being a dotted "
"import string") "import string")
@ -155,7 +154,7 @@ class MonkeyPatch:
""" """
__tracebackhide__ = True __tracebackhide__ = True
if name is notset: if name is notset:
if not isinstance(target, _basestring): if not isinstance(target, six.string_types):
raise TypeError("use delattr(target, name) or " raise TypeError("use delattr(target, name) or "
"delattr(target) with target being a dotted " "delattr(target) with target being a dotted "
"import string") "import string")

View File

@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function
import sys import sys
import py
from _pytest import unittest, runner, python from _pytest import unittest, runner, python
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -66,7 +65,7 @@ def is_potential_nosetest(item):
def call_optional(obj, name): def call_optional(obj, name):
method = getattr(obj, name, None) method = getattr(obj, name, None)
isfixture = hasattr(method, "_pytestfixturefunction") 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 # If there's any problems allow the exception to raise rather than
# silently ignoring them # silently ignoring them
method() method()

View File

@ -62,14 +62,21 @@ def exit(msg):
exit.Exception = Exit exit.Exception = Exit
def skip(msg=""): def skip(msg="", **kwargs):
""" skip an executing test with the given message. Note: it's usually """ 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 better to use the pytest.mark.skipif marker to declare a test to be
skipped under certain conditions like mismatching platforms or skipped under certain conditions like mismatching platforms or
dependencies. See the pytest_skipping plugin for details. 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 __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 keyword arguments: {0}'.format(keys))
raise Skipped(msg=msg, allow_module_level=allow_module_level)
skip.Exception = Skipped skip.Exception = Skipped

View File

@ -2,6 +2,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import pytest import pytest
import six
import sys import sys
import tempfile import tempfile
@ -16,7 +17,6 @@ def pytest_addoption(parser):
@pytest.hookimpl(trylast=True) @pytest.hookimpl(trylast=True)
def pytest_configure(config): def pytest_configure(config):
import py
if config.option.pastebin == "all": if config.option.pastebin == "all":
tr = config.pluginmanager.getplugin('terminalreporter') tr = config.pluginmanager.getplugin('terminalreporter')
# if no terminal reporter plugin is present, nothing we can do here; # if no terminal reporter plugin is present, nothing we can do here;
@ -25,15 +25,15 @@ def pytest_configure(config):
if tr is not None: if tr is not None:
# pastebin file will be utf-8 encoded binary file # pastebin file will be utf-8 encoded binary file
config._pastebinfile = tempfile.TemporaryFile('w+b') config._pastebinfile = tempfile.TemporaryFile('w+b')
oldwrite = tr._tw.write oldwrite = tr.writer.write
def tee_write(s, **kwargs): def tee_write(s, **kwargs):
oldwrite(s, **kwargs) oldwrite(s, **kwargs)
if py.builtin._istext(s): if isinstance(s, six.text_type):
s = s.encode('utf-8') s = s.encode('utf-8')
config._pastebinfile.write(s) config._pastebinfile.write(s)
tr._tw.write = tee_write tr.writer.write = tee_write
def pytest_unconfigure(config): def pytest_unconfigure(config):
@ -45,7 +45,7 @@ def pytest_unconfigure(config):
del config._pastebinfile del config._pastebinfile
# undo our patching in the terminal reporter # undo our patching in the terminal reporter
tr = config.pluginmanager.getplugin('terminalreporter') tr = config.pluginmanager.getplugin('terminalreporter')
del tr._tw.__dict__['write'] del tr.writer.__dict__['write']
# write summary # write summary
tr.write_sep("=", "Sending information to Paste Service") tr.write_sep("=", "Sending information to Paste Service")
pastebinurl = create_new_paste(sessionlog) pastebinurl = create_new_paste(sessionlog)

View File

@ -7,6 +7,7 @@ import os
import platform import platform
import re import re
import subprocess import subprocess
import six
import sys import sys
import time import time
import traceback import traceback
@ -22,6 +23,9 @@ from _pytest.main import Session, EXIT_OK
from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.assertion.rewrite import AssertionRewritingHook
PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py")
def pytest_addoption(parser): def pytest_addoption(parser):
# group = parser.getgroup("pytester", "pytester (self-tests) options") # group = parser.getgroup("pytester", "pytester (self-tests) options")
parser.addoption('--lsof', parser.addoption('--lsof',
@ -35,14 +39,6 @@ def pytest_addoption(parser):
def pytest_configure(config): 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"): if config.getvalue("lsof"):
checker = LsofFdLeakChecker() checker = LsofFdLeakChecker()
if checker.matching_platform(): if checker.matching_platform():
@ -114,12 +110,9 @@ class LsofFdLeakChecker(object):
# XXX copied from execnet's conftest.py - needs to be merged # XXX copied from execnet's conftest.py - needs to be merged
winpymap = { winpymap = {
'python2.7': r'C:\Python27\python.exe', '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.4': r'C:\Python34\python.exe',
'python3.5': r'C:\Python35\python.exe', 'python3.5': r'C:\Python35\python.exe',
'python3.6': r'C:\Python36\python.exe',
} }
@ -145,8 +138,7 @@ def getexecutable(name, cache={}):
return executable return executable
@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4", @pytest.fixture(params=['python2.7', 'python3.4', 'pypy', 'pypy3'])
'pypy', 'pypy3'])
def anypython(request): def anypython(request):
name = request.param name = request.param
executable = getexecutable(name) executable = getexecutable(name)
@ -418,16 +410,8 @@ class Testdir:
def __init__(self, request, tmpdir_factory): def __init__(self, request, tmpdir_factory):
self.request = request self.request = request
self._mod_collections = WeakKeyDictionary() self._mod_collections = WeakKeyDictionary()
# XXX remove duplication with tmpdir plugin
basetmp = tmpdir_factory.ensuretemp("testdir")
name = request.function.__name__ name = request.function.__name__
for i in range(100): self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
try:
tmpdir = basetmp.mkdir(name + str(i))
except py.error.EEXIST:
continue
break
self.tmpdir = tmpdir
self.plugins = [] self.plugins = []
self._savesyspath = (list(sys.path), list(sys.meta_path)) self._savesyspath = (list(sys.path), list(sys.meta_path))
self._savemodulekeys = set(sys.modules) self._savemodulekeys = set(sys.modules)
@ -486,29 +470,24 @@ class Testdir:
if not hasattr(self, '_olddir'): if not hasattr(self, '_olddir'):
self._olddir = old 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()) items = list(kwargs.items())
def to_text(s):
return s.decode(encoding) if isinstance(s, bytes) else six.text_type(s)
if args: if args:
source = py.builtin._totext("\n").join( source = u"\n".join(to_text(x) for x in args)
map(py.builtin._totext, args)) + py.builtin._totext("\n")
basename = self.request.function.__name__ basename = self.request.function.__name__
items.insert(0, (basename, source)) items.insert(0, (basename, source))
ret = None ret = None
for name, value in items: for basename, value in items:
p = self.tmpdir.join(name).new(ext=ext) p = self.tmpdir.join(basename).new(ext=ext)
p.dirpath().ensure_dir() p.dirpath().ensure_dir()
source = Source(value) source = Source(value)
source = u"\n".join(to_text(line) for line in source.lines)
def my_totext(s, encoding="utf-8"): p.write(source.strip().encode(encoding), "wb")
if py.builtin._isbytes(s):
s = py.builtin._totext(s, encoding=encoding)
return s
source_unicode = "\n".join([my_totext(line) for line in source.lines])
source = py.builtin._totext(source_unicode)
content = source.strip().encode(encoding) # + "\n"
# content = content.rstrip() + "\n"
p.write(content, "wb")
if ret is None: if ret is None:
ret = p ret = p
return ret return ret
@ -975,7 +954,7 @@ class Testdir:
def _getpytestargs(self): def _getpytestargs(self):
# we cannot use "(sys.executable,script)" # we cannot use "(sys.executable,script)"
# because on windows the script is e.g. a pytest.exe # 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): def runpython(self, script):
"""Run a python script using sys.executable as interpreter. """Run a python script using sys.executable as interpreter.

View File

@ -6,19 +6,23 @@ import inspect
import sys import sys
import os import os
import collections import collections
import warnings
from textwrap import dedent from textwrap import dedent
from itertools import count from itertools import count
import py import py
import six
from _pytest.mark import MarkerError from _pytest.mark import MarkerError
from _pytest.config import hookimpl from _pytest.config import hookimpl
import _pytest import _pytest
import _pytest._pluggy as pluggy import pluggy
from _pytest import fixtures from _pytest import fixtures
from _pytest import main from _pytest import main
from _pytest import deprecated
from _pytest.compat import ( from _pytest.compat import (
isclass, isfunction, is_generator, _ascii_escaped, isclass, isfunction, is_generator, ascii_escaped,
REGEX_TYPE, STRING_TYPES, NoneType, NOTSET, REGEX_TYPE, STRING_TYPES, NoneType, NOTSET,
get_real_func, getfslineno, safe_getattr, get_real_func, getfslineno, safe_getattr,
safe_str, getlocation, enum, safe_str, getlocation, enum,
@ -327,7 +331,7 @@ class PyCollector(PyobjMixin, main.Collector):
if name in seen: if name in seen:
continue continue
seen[name] = True seen[name] = True
res = self.makeitem(name, obj) res = self._makeitem(name, obj)
if res is None: if res is None:
continue continue
if not isinstance(res, list): if not isinstance(res, list):
@ -337,6 +341,10 @@ class PyCollector(PyobjMixin, main.Collector):
return values return values
def makeitem(self, name, obj): 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 # assert self.ihook.fspath == self.fspath, self
return self.ihook.pytest_pycollect_makeitem( return self.ihook.pytest_pycollect_makeitem(
collector=self, name=name, obj=obj) collector=self, name=name, obj=obj)
@ -613,7 +621,7 @@ class Generator(FunctionMixin, PyCollector):
if not isinstance(obj, (tuple, list)): if not isinstance(obj, (tuple, list)):
obj = (obj,) obj = (obj,)
# explicit naming # explicit naming
if isinstance(obj[0], py.builtin._basestring): if isinstance(obj[0], six.string_types):
name = obj[0] name = obj[0]
obj = obj[1:] obj = obj[1:]
else: else:
@ -644,14 +652,14 @@ class CallSpec2(object):
self._globalid_args = set() self._globalid_args = set()
self._globalparam = NOTSET self._globalparam = NOTSET
self._arg2scopenum = {} # used for sorting parametrized resources self._arg2scopenum = {} # used for sorting parametrized resources
self.keywords = {} self.marks = []
self.indices = {} self.indices = {}
def copy(self, metafunc): def copy(self, metafunc):
cs = CallSpec2(self.metafunc) cs = CallSpec2(self.metafunc)
cs.funcargs.update(self.funcargs) cs.funcargs.update(self.funcargs)
cs.params.update(self.params) cs.params.update(self.params)
cs.keywords.update(self.keywords) cs.marks.extend(self.marks)
cs.indices.update(self.indices) cs.indices.update(self.indices)
cs._arg2scopenum.update(self._arg2scopenum) cs._arg2scopenum.update(self._arg2scopenum)
cs._idlist = list(self._idlist) cs._idlist = list(self._idlist)
@ -676,7 +684,7 @@ class CallSpec2(object):
def id(self): def id(self):
return "-".join(map(str, filter(None, self._idlist))) return "-".join(map(str, filter(None, self._idlist)))
def setmulti(self, valtypes, argnames, valset, id, keywords, scopenum, def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum,
param_index): param_index):
for arg, val in zip(argnames, valset): for arg, val in zip(argnames, valset):
self._checkargnotcontained(arg) self._checkargnotcontained(arg)
@ -685,7 +693,7 @@ class CallSpec2(object):
self.indices[arg] = param_index self.indices[arg] = param_index
self._arg2scopenum[arg] = scopenum self._arg2scopenum[arg] = scopenum
self._idlist.append(id) self._idlist.append(id)
self.keywords.update(keywords) self.marks.extend(marks)
def setall(self, funcargs, id, param): def setall(self, funcargs, id, param):
for x in funcargs: for x in funcargs:
@ -725,7 +733,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
self.cls = cls self.cls = cls
self._calls = [] self._calls = []
self._ids = py.builtin.set() self._ids = set()
self._arg2fixturedefs = fixtureinfo.name2fixturedefs self._arg2fixturedefs = fixtureinfo.name2fixturedefs
def parametrize(self, argnames, argvalues, indirect=False, ids=None, def parametrize(self, argnames, argvalues, indirect=False, ids=None,
@ -768,30 +776,12 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
to set a dynamic scope using test context or configuration. to set a dynamic scope using test context or configuration.
""" """
from _pytest.fixtures import scope2index from _pytest.fixtures import scope2index
from _pytest.mark import MARK_GEN, ParameterSet from _pytest.mark import ParameterSet
from py.io import saferepr from py.io import saferepr
argnames, parameters = ParameterSet._for_parameterize(
if not isinstance(argnames, (tuple, list)): argnames, argvalues, self.function)
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 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: if scope is None:
scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect) scope = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
@ -827,7 +817,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
raise ValueError('%d tests specified with %d ids' % ( raise ValueError('%d tests specified with %d ids' % (
len(parameters), len(ids))) len(parameters), len(ids)))
for id_value in 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)' msg = 'ids must be list of strings, found: %s (type: %s)'
raise ValueError(msg % (saferepr(id_value), type(id_value).__name__)) raise ValueError(msg % (saferepr(id_value), type(id_value).__name__))
ids = idmaker(argnames, parameters, idfn, ids, self.config) ids = idmaker(argnames, parameters, idfn, ids, self.config)
@ -841,8 +831,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
'equal to the number of names ({1})'.format( 'equal to the number of names ({1})'.format(
param.values, argnames)) param.values, argnames))
newcallspec = callspec.copy(self) newcallspec = callspec.copy(self)
newcallspec.setmulti(valtypes, argnames, param.values, a_id, newcallspec.setmulti2(valtypes, argnames, param.values, a_id,
param.deprecated_arg_dict, scopenum, param_index) param.marks, scopenum, param_index)
newcalls.append(newcallspec) newcalls.append(newcallspec)
self._calls = newcalls self._calls = newcalls
@ -921,7 +911,7 @@ def _idval(val, argname, idx, idfn, config=None):
msg += '\nUpdate your code as this will raise an error in pytest-4.0.' msg += '\nUpdate your code as this will raise an error in pytest-4.0.'
warnings.warn(msg, DeprecationWarning) warnings.warn(msg, DeprecationWarning)
if s: if s:
return _ascii_escaped(s) return ascii_escaped(s)
if config: if config:
hook_id = config.hook.pytest_make_parametrize_id( hook_id = config.hook.pytest_make_parametrize_id(
@ -930,11 +920,11 @@ def _idval(val, argname, idx, idfn, config=None):
return hook_id return hook_id
if isinstance(val, STRING_TYPES): if isinstance(val, STRING_TYPES):
return _ascii_escaped(val) return ascii_escaped(val)
elif isinstance(val, (float, int, bool, NoneType)): elif isinstance(val, (float, int, bool, NoneType)):
return str(val) return str(val)
elif isinstance(val, REGEX_TYPE): 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): elif enum is not None and isinstance(val, enum.Enum):
return str(val) return str(val)
elif isclass(val) and hasattr(val, '__name__'): elif isclass(val) and hasattr(val, '__name__'):
@ -950,7 +940,7 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
for val, argname in zip(parameterset.values, argnames)] for val, argname in zip(parameterset.values, argnames)]
return "-".join(this_id) return "-".join(this_id)
else: else:
return _ascii_escaped(ids[idx]) return ascii_escaped(ids[idx])
def idmaker(argnames, parametersets, idfn=None, ids=None, config=None): def idmaker(argnames, parametersets, idfn=None, ids=None, config=None):
@ -1112,7 +1102,13 @@ class Function(FunctionMixin, main.Item, fixtures.FuncargnamesCompatAttr):
self.keywords.update(self.obj.__dict__) self.keywords.update(self.obj.__dict__)
if callspec: if callspec:
self.callspec = 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: if keywords:
self.keywords.update(keywords) self.keywords.update(keywords)

View File

@ -2,8 +2,9 @@ import math
import sys import sys
import py import py
from six.moves import zip
from _pytest.compat import isclass, izip from _pytest.compat import isclass
from _pytest.outcomes import fail from _pytest.outcomes import fail
import _pytest._code import _pytest._code
@ -145,7 +146,7 @@ class ApproxSequence(ApproxBase):
return ApproxBase.__eq__(self, actual) return ApproxBase.__eq__(self, actual)
def _yield_comparisons(self, actual): def _yield_comparisons(self, actual):
return izip(actual, self.expected) return zip(actual, self.expected)
class ApproxScalar(ApproxBase): class ApproxScalar(ApproxBase):
@ -454,8 +455,7 @@ def raises(expected_exception, *args, **kwargs):
This helper produces a ``ExceptionInfo()`` object (see below). This helper produces a ``ExceptionInfo()`` object (see below).
If using Python 2.5 or above, you may use this function as a You may use this function as a context manager::
context manager::
>>> with raises(ZeroDivisionError): >>> with raises(ZeroDivisionError):
... 1/0 ... 1/0
@ -610,13 +610,6 @@ class RaisesContext(object):
__tracebackhide__ = True __tracebackhide__ = True
if tp[0] is None: if tp[0] is None:
fail(self.message) 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) self.excinfo.__init__(tp)
suppress_exception = issubclass(self.excinfo.type, self.expected_exception) suppress_exception = issubclass(self.excinfo.type, self.expected_exception)
if sys.version_info[0] == 2 and suppress_exception: if sys.version_info[0] == 2 and suppress_exception:

View File

@ -8,6 +8,8 @@ import py
import sys import sys
import warnings import warnings
import re
from _pytest.fixtures import yield_fixture from _pytest.fixtures import yield_fixture
from _pytest.outcomes import fail from _pytest.outcomes import fail
@ -98,10 +100,28 @@ def warns(expected_warning, *args, **kwargs):
>>> with warns(RuntimeWarning): >>> with warns(RuntimeWarning):
... warnings.warn("my warning", 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...
""" """
wcheck = WarningsChecker(expected_warning) match_expr = None
if not args: 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): elif isinstance(args[0], str):
code, = args code, = args
assert isinstance(code, str) assert isinstance(code, str)
@ -109,12 +129,12 @@ def warns(expected_warning, *args, **kwargs):
loc = frame.f_locals.copy() loc = frame.f_locals.copy()
loc.update(kwargs) loc.update(kwargs)
with wcheck: with WarningsChecker(expected_warning, match_expr=match_expr):
code = _pytest._code.Source(code).compile() code = _pytest._code.Source(code).compile()
py.builtin.exec_(code, frame.f_globals, loc) py.builtin.exec_(code, frame.f_globals, loc)
else: else:
func = args[0] func = args[0]
with wcheck: with WarningsChecker(expected_warning, match_expr=match_expr):
return func(*args[1:], **kwargs) return func(*args[1:], **kwargs)
@ -174,7 +194,7 @@ class WarningsRecorder(warnings.catch_warnings):
class WarningsChecker(WarningsRecorder): class WarningsChecker(WarningsRecorder):
def __init__(self, expected_warning=None): def __init__(self, expected_warning=None, match_expr=None):
super(WarningsChecker, self).__init__() super(WarningsChecker, self).__init__()
msg = ("exceptions must be old-style classes or " msg = ("exceptions must be old-style classes or "
@ -189,6 +209,7 @@ class WarningsChecker(WarningsRecorder):
raise TypeError(msg % type(expected_warning)) raise TypeError(msg % type(expected_warning))
self.expected_warning = expected_warning self.expected_warning = expected_warning
self.match_expr = match_expr
def __exit__(self, *exc_info): def __exit__(self, *exc_info):
super(WarningsChecker, self).__exit__(*exc_info) super(WarningsChecker, self).__exit__(*exc_info)
@ -203,3 +224,13 @@ class WarningsChecker(WarningsRecorder):
"The list of emitted warnings is: {1}.".format( "The list of emitted warnings is: {1}.".format(
self.expected_warning, self.expected_warning,
[each.message for each in self])) [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]))

View File

@ -431,7 +431,7 @@ class SetupState(object):
is called at the end of teardown_all(). is called at the end of teardown_all().
""" """
assert colitem and not isinstance(colitem, tuple) 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 :/ # assert colitem in self.stack # some unit tests don't setup stack :/
self._finalizers.setdefault(colitem, []).append(finalizer) self._finalizers.setdefault(colitem, []).append(finalizer)

View File

@ -44,7 +44,7 @@ def _show_fixture_action(fixturedef, msg):
config = fixturedef._fixturemanager.config config = fixturedef._fixturemanager.config
capman = config.pluginmanager.getplugin('capturemanager') capman = config.pluginmanager.getplugin('capturemanager')
if capman: if capman:
out, err = capman.suspendcapture() out, err = capman.suspend_global_capture()
tw = config.get_terminal_writer() tw = config.get_terminal_writer()
tw.line() tw.line()
@ -63,7 +63,7 @@ def _show_fixture_action(fixturedef, msg):
tw.write('[{0}]'.format(fixturedef.cached_param)) tw.write('[{0}]'.format(fixturedef.cached_param))
if capman: if capman:
capman.resumecapture() capman.resume_global_capture()
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)

View File

@ -2,10 +2,10 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import os import os
import six
import sys import sys
import traceback import traceback
import py
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.mark import MarkInfo, MarkDecorator from _pytest.mark import MarkInfo, MarkDecorator
from _pytest.outcomes import fail, skip, xfail, TEST_OUTCOME from _pytest.outcomes import fail, skip, xfail, TEST_OUTCOME
@ -60,22 +60,31 @@ def pytest_configure(config):
) )
class MarkEvaluator: class MarkEvaluator(object):
def __init__(self, item, name): def __init__(self, item, name):
self.item = item self.item = item
self.name = name self._marks = None
self._mark = None
@property self._mark_name = name
def holder(self):
return self.item.keywords.get(self.name)
def __bool__(self): def __bool__(self):
return bool(self.holder) self._marks = self._get_marks()
return bool(self._marks)
__nonzero__ = __bool__ __nonzero__ = __bool__
def wasvalid(self): def wasvalid(self):
return not hasattr(self, 'exc') return not hasattr(self, 'exc')
def _get_marks(self):
keyword = self.item.keywords.get(self._mark_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): def invalidraise(self, exc):
raises = self.get('raises') raises = self.get('raises')
if not raises: if not raises:
@ -95,7 +104,7 @@ class MarkEvaluator:
fail("Error evaluating %r expression\n" fail("Error evaluating %r expression\n"
" %s\n" " %s\n"
"%s" "%s"
% (self.name, self.expr, "\n".join(msg)), % (self._mark_name, self.expr, "\n".join(msg)),
pytrace=False) pytrace=False)
def _getglobals(self): def _getglobals(self):
@ -107,24 +116,24 @@ class MarkEvaluator:
def _istrue(self): def _istrue(self):
if hasattr(self, 'result'): if hasattr(self, 'result'):
return self.result return self.result
if self.holder: self._marks = self._get_marks()
if self.holder.args or 'condition' in self.holder.kwargs:
if self._marks:
self.result = False self.result = False
# "holder" might be a MarkInfo or a MarkDecorator; only for mark in self._marks:
# MarkInfo keeps track of all parameters it received in an self._mark = mark
# _arglist attribute if 'condition' in mark.kwargs:
marks = getattr(self.holder, '_marks', None) \ args = (mark.kwargs['condition'],)
or [self.holder.mark] else:
for _, args, kwargs in marks: args = mark.args
if 'condition' in kwargs:
args = (kwargs['condition'],)
for expr in args: for expr in args:
self.expr = expr self.expr = expr
if isinstance(expr, py.builtin._basestring): if isinstance(expr, six.string_types):
d = self._getglobals() d = self._getglobals()
result = cached_eval(self.item.config, expr, d) result = cached_eval(self.item.config, expr, d)
else: else:
if "reason" not in kwargs: if "reason" not in mark.kwargs:
# XXX better be checked at collection time # XXX better be checked at collection time
msg = "you need to specify reason=STRING " \ msg = "you need to specify reason=STRING " \
"when using booleans as conditions." "when using booleans as conditions."
@ -132,15 +141,20 @@ class MarkEvaluator:
result = bool(expr) result = bool(expr)
if result: if result:
self.result = True self.result = True
self.reason = kwargs.get('reason', None) self.reason = mark.kwargs.get('reason', None)
self.expr = expr self.expr = expr
return self.result return self.result
else:
if not args:
self.result = True self.result = True
return getattr(self, 'result', False) self.reason = mark.kwargs.get('reason', None)
return self.result
return False
def get(self, attr, default=None): 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): def getexplanation(self):
expl = getattr(self, 'reason', None) or self.get('reason', None) expl = getattr(self, 'reason', None) or self.get('reason', None)
@ -155,17 +169,17 @@ class MarkEvaluator:
@hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
def pytest_runtest_setup(item): def pytest_runtest_setup(item):
# Check if skip or skipif are specified as pytest marks # Check if skip or skipif are specified as pytest marks
item._skipped_by_mark = False
skipif_info = item.keywords.get('skipif') skipif_info = item.keywords.get('skipif')
if isinstance(skipif_info, (MarkInfo, MarkDecorator)): if isinstance(skipif_info, (MarkInfo, MarkDecorator)):
eval_skipif = MarkEvaluator(item, 'skipif') eval_skipif = MarkEvaluator(item, 'skipif')
if eval_skipif.istrue(): if eval_skipif.istrue():
item._evalskip = eval_skipif item._skipped_by_mark = True
skip(eval_skipif.getexplanation()) skip(eval_skipif.getexplanation())
skip_info = item.keywords.get('skip') skip_info = item.keywords.get('skip')
if isinstance(skip_info, (MarkInfo, MarkDecorator)): if isinstance(skip_info, (MarkInfo, MarkDecorator)):
item._evalskip = True item._skipped_by_mark = True
if 'reason' in skip_info.kwargs: if 'reason' in skip_info.kwargs:
skip(skip_info.kwargs['reason']) skip(skip_info.kwargs['reason'])
elif skip_info.args: elif skip_info.args:
@ -212,7 +226,6 @@ def pytest_runtest_makereport(item, call):
outcome = yield outcome = yield
rep = outcome.get_result() rep = outcome.get_result()
evalxfail = getattr(item, '_evalxfail', None) evalxfail = getattr(item, '_evalxfail', None)
evalskip = getattr(item, '_evalskip', None)
# unitttest special case, see setting of _unexpectedsuccess # unitttest special case, see setting of _unexpectedsuccess
if hasattr(item, '_unexpectedsuccess') and rep.when == "call": if hasattr(item, '_unexpectedsuccess') and rep.when == "call":
from _pytest.compat import _is_unittest_unexpected_success_a_failure from _pytest.compat import _is_unittest_unexpected_success_a_failure
@ -248,7 +261,7 @@ def pytest_runtest_makereport(item, call):
else: else:
rep.outcome = "passed" rep.outcome = "passed"
rep.wasxfail = explanation 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 # skipped by mark.skipif; change the location of the failure
# to point to the item definition, otherwise it will display # to point to the item definition, otherwise it will display
# the location of where the skip exception was raised within pytest # the location of where the skip exception was raised within pytest
@ -295,9 +308,9 @@ def pytest_terminal_summary(terminalreporter):
show_simple(terminalreporter, lines, 'passed', "PASSED %s") show_simple(terminalreporter, lines, 'passed', "PASSED %s")
if lines: if lines:
tr._tw.sep("=", "short test summary info") tr.writer.sep("=", "short test summary info")
for line in lines: for line in lines:
tr._tw.line(line) tr.writer.line(line)
def show_simple(terminalreporter, lines, stat, format): def show_simple(terminalreporter, lines, stat, format):
@ -345,6 +358,13 @@ def folded_skips(skipped):
for event in skipped: for event in skipped:
key = event.longrepr key = event.longrepr
assert len(key) == 3, (event, key) 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
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) d.setdefault(key, []).append(event)
values = [] values = []
for key, events in d.items(): for key, events in d.items():
@ -367,6 +387,11 @@ def show_skipped(terminalreporter, lines):
for num, fspath, lineno, reason in fskips: for num, fspath, lineno, reason in fskips:
if reason.startswith("Skipped: "): if reason.startswith("Skipped: "):
reason = reason[9:] reason = reason[9:]
if lineno is not None:
lines.append( lines.append(
"SKIP [%d] %s:%d: %s" % "SKIP [%d] %s:%d: %s" %
(num, fspath, lineno + 1, reason)) (num, fspath, lineno + 1, reason))
else:
lines.append(
"SKIP [%d] %s: %s" %
(num, fspath, reason))

View File

@ -5,16 +5,19 @@ This is a good source for looking at the various reporting hooks.
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import itertools import itertools
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \ import platform
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
import pytest
import py
import sys import sys
import time import time
import platform import warnings
import pluggy
import py
import six
import pytest
from _pytest import nodes from _pytest import nodes
import _pytest._pluggy as pluggy from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
def pytest_addoption(parser): def pytest_addoption(parser):
@ -137,13 +140,22 @@ class TerminalReporter:
self.startdir = py.path.local() self.startdir = py.path.local()
if file is None: if file is None:
file = sys.stdout file = sys.stdout
self._tw = self.writer = _pytest.config.create_terminal_writer(config, self._writer = _pytest.config.create_terminal_writer(config, file)
file)
self.currentfspath = None self.currentfspath = None
self.reportchars = getreportopt(config) self.reportchars = getreportopt(config)
self.hasmarkup = self._tw.hasmarkup self.hasmarkup = self.writer.hasmarkup
self.isatty = file.isatty() 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): def hasopt(self, char):
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char) char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
return char in self.reportchars return char in self.reportchars
@ -153,32 +165,32 @@ class TerminalReporter:
if fspath != self.currentfspath: if fspath != self.currentfspath:
self.currentfspath = fspath self.currentfspath = fspath
fspath = self.startdir.bestrelpath(fspath) fspath = self.startdir.bestrelpath(fspath)
self._tw.line() self.writer.line()
self._tw.write(fspath + " ") self.writer.write(fspath + " ")
self._tw.write(res) self.writer.write(res)
def write_ensure_prefix(self, prefix, extra="", **kwargs): def write_ensure_prefix(self, prefix, extra="", **kwargs):
if self.currentfspath != prefix: if self.currentfspath != prefix:
self._tw.line() self.writer.line()
self.currentfspath = prefix self.currentfspath = prefix
self._tw.write(prefix) self.writer.write(prefix)
if extra: if extra:
self._tw.write(extra, **kwargs) self.writer.write(extra, **kwargs)
self.currentfspath = -2 self.currentfspath = -2
def ensure_newline(self): def ensure_newline(self):
if self.currentfspath: if self.currentfspath:
self._tw.line() self.writer.line()
self.currentfspath = None self.currentfspath = None
def write(self, content, **markup): def write(self, content, **markup):
self._tw.write(content, **markup) self.writer.write(content, **markup)
def write_line(self, line, **markup): def write_line(self, line, **markup):
if not py.builtin._istext(line): if not isinstance(line, six.text_type):
line = py.builtin.text(line, errors="replace") line = six.text_type(line, errors="replace")
self.ensure_newline() self.ensure_newline()
self._tw.line(line, **markup) self.writer.line(line, **markup)
def rewrite(self, line, **markup): def rewrite(self, line, **markup):
""" """
@ -191,25 +203,25 @@ class TerminalReporter:
""" """
erase = markup.pop('erase', False) erase = markup.pop('erase', False)
if erase: if erase:
fill_count = self._tw.fullwidth - len(line) fill_count = self.writer.fullwidth - len(line)
fill = ' ' * fill_count fill = ' ' * fill_count
else: else:
fill = '' fill = ''
line = str(line) 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): def write_sep(self, sep, title=None, **markup):
self.ensure_newline() self.ensure_newline()
self._tw.sep(sep, title, **markup) self.writer.sep(sep, title, **markup)
def section(self, title, sep="=", **kw): def section(self, title, sep="=", **kw):
self._tw.sep(sep, title, **kw) self.writer.sep(sep, title, **kw)
def line(self, msg, **kw): def line(self, msg, **kw):
self._tw.line(msg, **kw) self.writer.line(msg, **kw)
def pytest_internalerror(self, excrepr): 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) self.write_line("INTERNALERROR> " + line)
return 1 return 1
@ -253,7 +265,7 @@ class TerminalReporter:
if not hasattr(rep, 'node') and self.showfspath: if not hasattr(rep, 'node') and self.showfspath:
self.write_fspath_result(rep.nodeid, letter) self.write_fspath_result(rep.nodeid, letter)
else: else:
self._tw.write(letter) self.writer.write(letter)
else: else:
if isinstance(word, tuple): if isinstance(word, tuple):
word, markup = word word, markup = word
@ -264,16 +276,18 @@ class TerminalReporter:
markup = {'red': True} markup = {'red': True}
elif rep.skipped: elif rep.skipped:
markup = {'yellow': True} markup = {'yellow': True}
else:
markup = {}
line = self._locationline(rep.nodeid, *rep.location) line = self._locationline(rep.nodeid, *rep.location)
if not hasattr(rep, 'node'): if not hasattr(rep, 'node'):
self.write_ensure_prefix(line, word, **markup) self.write_ensure_prefix(line, word, **markup)
# self._tw.write(word, **markup) # self.writer.write(word, **markup)
else: else:
self.ensure_newline() self.ensure_newline()
if hasattr(rep, 'node'): if hasattr(rep, 'node'):
self._tw.write("[%s] " % rep.node.gateway.id) self.writer.write("[%s] " % rep.node.gateway.id)
self._tw.write(word, **markup) self.writer.write(word, **markup)
self._tw.write(" " + line) self.writer.write(" " + line)
self.currentfspath = -2 self.currentfspath = -2
def pytest_collection(self): def pytest_collection(self):
@ -359,9 +373,9 @@ class TerminalReporter:
if self.config.option.collectonly: if self.config.option.collectonly:
self._printcollecteditems(session.items) self._printcollecteditems(session.items)
if self.stats.get('failed'): if self.stats.get('failed'):
self._tw.sep("!", "collection failures") self.writer.sep("!", "collection failures")
for rep in self.stats.get('failed'): for rep in self.stats.get('failed'):
rep.toterminal(self._tw) rep.toterminal(self.writer)
return 1 return 1
return 0 return 0
lines = self.config.hook.pytest_report_collectionfinish( lines = self.config.hook.pytest_report_collectionfinish(
@ -379,12 +393,12 @@ class TerminalReporter:
name = item.nodeid.split('::', 1)[0] name = item.nodeid.split('::', 1)[0]
counts[name] = counts.get(name, 0) + 1 counts[name] = counts.get(name, 0) + 1
for name, count in sorted(counts.items()): for name, count in sorted(counts.items()):
self._tw.line("%s: %d" % (name, count)) self.writer.line("%s: %d" % (name, count))
else: else:
for item in items: for item in items:
nodeid = item.nodeid nodeid = item.nodeid
nodeid = nodeid.replace("::()::", "::") nodeid = nodeid.replace("::()::", "::")
self._tw.line(nodeid) self.writer.line(nodeid)
return return
stack = [] stack = []
indent = "" indent = ""
@ -399,13 +413,13 @@ class TerminalReporter:
# if col.name == "()": # if col.name == "()":
# continue # continue
indent = (len(stack) - 1) * " " indent = (len(stack) - 1) * " "
self._tw.line("%s%s" % (indent, col)) self.writer.line("%s%s" % (indent, col))
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(self, exitstatus): def pytest_sessionfinish(self, exitstatus):
outcome = yield outcome = yield
outcome.get_result() outcome.get_result()
self._tw.line("") self.writer.line("")
summary_exit_codes = ( summary_exit_codes = (
EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR, EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR,
EXIT_NOTESTSCOLLECTED) EXIT_NOTESTSCOLLECTED)
@ -435,10 +449,10 @@ class TerminalReporter:
self.write_sep("!", msg) self.write_sep("!", msg)
if "KeyboardInterrupt" in msg: if "KeyboardInterrupt" in msg:
if self.config.option.fulltrace: if self.config.option.fulltrace:
excrepr.toterminal(self._tw) excrepr.toterminal(self.writer)
else: else:
self._tw.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True) self.writer.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True)
excrepr.reprcrash.toterminal(self._tw) excrepr.reprcrash.toterminal(self.writer)
def _locationline(self, nodeid, fspath, lineno, domain): def _locationline(self, nodeid, fspath, lineno, domain):
def mkrel(nodeid): def mkrel(nodeid):
@ -494,14 +508,14 @@ class TerminalReporter:
grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config)) grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))
self.write_sep("=", "warnings summary", yellow=True, bold=False) self.write_sep("=", "warnings summary", yellow=True, bold=False)
for location, warnings in grouped: for location, warning_records in grouped:
self._tw.line(str(location) or '<undetermined location>') self.writer.line(str(location) or '<undetermined location>')
for w in warnings: for w in warning_records:
lines = w.message.splitlines() lines = w.message.splitlines()
indented = '\n'.join(' ' + x for x in lines) indented = '\n'.join(' ' + x for x in lines)
self._tw.line(indented) self.writer.line(indented)
self._tw.line() self.writer.line()
self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html') self.writer.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html')
def summary_passes(self): def summary_passes(self):
if self.config.option.tbstyle != "no": if self.config.option.tbstyle != "no":
@ -518,10 +532,10 @@ class TerminalReporter:
def print_teardown_sections(self, rep): def print_teardown_sections(self, rep):
for secname, content in rep.sections: for secname, content in rep.sections:
if 'teardown' in secname: if 'teardown' in secname:
self._tw.sep('-', secname) self.writer.sep('-', secname)
if content[-1:] == "\n": if content[-1:] == "\n":
content = content[:-1] content = content[:-1]
self._tw.line(content) self.writer.line(content)
def summary_failures(self): def summary_failures(self):
if self.config.option.tbstyle != "no": if self.config.option.tbstyle != "no":
@ -561,12 +575,12 @@ class TerminalReporter:
self._outrep_summary(rep) self._outrep_summary(rep)
def _outrep_summary(self, rep): def _outrep_summary(self, rep):
rep.toterminal(self._tw) rep.toterminal(self.writer)
for secname, content in rep.sections: for secname, content in rep.sections:
self._tw.sep("-", secname) self.writer.sep("-", secname)
if content[-1:] == "\n": if content[-1:] == "\n":
content = content[:-1] content = content[:-1]
self._tw.line(content) self.writer.line(content)
def summary_stats(self): def summary_stats(self):
session_duration = time.time() - self._sessionstarttime session_duration = time.time() - self._sessionstarttime

View File

@ -9,7 +9,6 @@ import _pytest._code
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.outcomes import fail, skip, xfail from _pytest.outcomes import fail, skip, xfail
from _pytest.python import transfer_markers, Class, Module, Function from _pytest.python import transfer_markers, Class, Module, Function
from _pytest.skipping import MarkEvaluator
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
@ -134,8 +133,7 @@ class TestCaseFunction(Function):
try: try:
skip(reason) skip(reason)
except skip.Exception: except skip.Exception:
self._evalskip = MarkEvaluator(self, 'SkipTest') self._skipped_by_mark = True
self._evalskip.result = True
self._addexcinfo(sys.exc_info()) self._addexcinfo(sys.exc_info())
def addExpectedFailure(self, testcase, rawexcinfo, reason=""): def addExpectedFailure(self, testcase, rawexcinfo, reason=""):

View File

@ -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==<version> --no-compile --target=_pytest/vendored_packages
```
And commit the modified files. The `pluggy-<version>.dist-info` directory
created by `pip` should be added as well.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"}

View File

@ -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, "<temp>", 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))
)

View File

@ -72,8 +72,8 @@ def catch_warnings_for_item(item):
unicode_warning = False unicode_warning = False
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args): 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] new_args = [compat.ascii_escaped(m) for m in warn_msg.args]
unicode_warning = warn_msg.args != new_args unicode_warning = list(warn_msg.args) != new_args
warn_msg.args = new_args warn_msg.args = new_args
msg = warnings.formatwarning( msg = warnings.formatwarning(

View File

@ -10,9 +10,7 @@ environment:
- TOXENV: "coveralls" - TOXENV: "coveralls"
# note: please use "tox --listenvs" to populate the build matrix below # note: please use "tox --listenvs" to populate the build matrix below
- TOXENV: "linting" - TOXENV: "linting"
- TOXENV: "py26"
- TOXENV: "py27" - TOXENV: "py27"
- TOXENV: "py33"
- TOXENV: "py34" - TOXENV: "py34"
- TOXENV: "py35" - TOXENV: "py35"
- TOXENV: "py36" - TOXENV: "py36"
@ -21,10 +19,12 @@ environment:
- TOXENV: "py27-xdist" - TOXENV: "py27-xdist"
- TOXENV: "py27-trial" - TOXENV: "py27-trial"
- TOXENV: "py27-numpy" - TOXENV: "py27-numpy"
- TOXENV: "py27-pluggymaster"
- TOXENV: "py36-pexpect" - TOXENV: "py36-pexpect"
- TOXENV: "py36-xdist" - TOXENV: "py36-xdist"
- TOXENV: "py36-trial" - TOXENV: "py36-trial"
- TOXENV: "py36-numpy" - TOXENV: "py36-numpy"
- TOXENV: "py36-pluggymaster"
- TOXENV: "py27-nobyte" - TOXENV: "py27-nobyte"
- TOXENV: "doctesting" - TOXENV: "doctesting"
- TOXENV: "py35-freeze" - TOXENV: "py35-freeze"

1
changelog/1993.bugfix Normal file
View File

@ -0,0 +1 @@
Resume output capturing after ``capsys/capfd.disabled()`` context manager.

1
changelog/2236.removal Normal file
View File

@ -0,0 +1 @@
- Remove internal ``_preloadplugins()`` function. This removal is part of the ``pytest_namespace()`` hook deprecation.

4
changelog/2267.feature Normal file
View File

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

4
changelog/2491.bugfix Normal file
View File

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

1
changelog/2549.feature Normal file
View File

@ -0,0 +1 @@
Report only once tests with global ``pytestmark`` variable.

1
changelog/2641.trivial Normal file
View File

@ -0,0 +1 @@
pytest now depends on `attrs <https://pypi.org/project/attrs/>`_ for internal structures to ease code maintainability.

1
changelog/2642.trivial Normal file
View File

@ -0,0 +1 @@
Refactored internal Python 2/3 compatibility code to use ``six``.

2
changelog/2672.removal Normal file
View File

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

1
changelog/2675.removal Normal file
View File

@ -0,0 +1 @@
remove ParameterSet.deprecated_arg_dict - its not a public api and the lack of the underscore was a naming error.

1
changelog/2708.feature Normal file
View File

@ -0,0 +1 @@
Match ``warns`` signature to ``raises`` by adding ``match`` keyworkd.

1
changelog/2709.bugfix Normal file
View File

@ -0,0 +1 @@
``capsys`` and ``capfd`` can now be used by other fixtures.

1
changelog/2719.trivial Normal file
View File

@ -0,0 +1 @@
Stop vendoring ``pluggy`` - we're missing out on it's latest changes for not much benefit

1
changelog/2734.trivial Normal file
View File

@ -0,0 +1 @@
Internal refactor: simplify ascii string escaping by using the backslashreplace error handler in newer Python 3 versions.

1
changelog/2738.bugfix Normal file
View File

@ -0,0 +1 @@
Internal ``pytester`` plugin properly encodes ``bytes`` arguments to ``utf-8``.

1
changelog/2751.bugfix Normal file
View File

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

1
changelog/2767.removal Normal file
View File

@ -0,0 +1 @@
Remove the internal multi-typed attribute ``Node._evalskip`` and replace it with the boolean ``Node._skipped_by_mark``.

1
changelog/2767.trivial Normal file
View File

@ -0,0 +1 @@
* remove unnecessary mark evaluator in unittest plugin

3
changelog/2794.feature Normal file
View File

@ -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 <https://docs.pytest.org/en/latest/logging.html>`_.
This feature was introduced by merging the popular `pytest-catchlog <https://pypi.org/project/pytest-catchlog/>`_ plugin, thanks to `Thomas Hisch <https://github.com/thisch>`_. Be advised that during the merging the backward compatibility interface with the defunct ``pytest-capturelog`` has been dropped.

1
changelog/2803.removal Normal file
View File

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

1
changelog/2808.feature Normal file
View File

@ -0,0 +1 @@
Add ``allow_module_level`` kwarg to ``pytest.skip()``, enabling to skip the whole module.

1
changelog/2809.bugfix Normal file
View File

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

1
changelog/2812.removal Normal file
View File

@ -0,0 +1 @@
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``.

1
changelog/2824.feature Normal file
View File

@ -0,0 +1 @@
Allow setting ``file_or_dir``, ``-c``, and ``-o`` in PYTEST_ADDOPTS.

1
changelog/2845.bugfix Normal file
View File

@ -0,0 +1 @@
Change return value of pytest command when ``--maxfail`` is reached from ``2`` (interrupted) to ``1`` (failed).

1
changelog/2877.trivial Normal file
View File

@ -0,0 +1 @@
Internal move of the parameterset extraction to a more maintainable place.

1
changelog/2879.feature Normal file
View File

@ -0,0 +1 @@
Return stdout/stderr capture results as a ``namedtuple``, so ``out`` and ``err`` can be accessed by attribute.

1
changelog/2910.trivial Normal file
View File

@ -0,0 +1 @@
Internal refactoring to simplify scope node lookup.

1
changelog/502.feature Normal file
View File

@ -0,0 +1 @@
Implement feature to skip ``setup.py`` files when ran with ``--doctest-modules``.

View File

@ -30,6 +30,7 @@ Full pytest documentation
xunit_setup xunit_setup
plugins plugins
writing_plugins writing_plugins
logging
goodpractices goodpractices
pythonpath pythonpath

View File

@ -6,7 +6,7 @@ import py
import pytest import pytest
import _pytest._code import _pytest._code
pythonlist = ['python2.6', 'python2.7', 'python3.4', 'python3.5'] pythonlist = ['python2.7', 'python3.4', 'python3.5']
@pytest.fixture(params=pythonlist) @pytest.fixture(params=pythonlist)
def python1(request, tmpdir): def python1(request, tmpdir):
picklefile = tmpdir.join("data.pickle") picklefile = tmpdir.join("data.pickle")

View File

@ -1,7 +1,7 @@
Installation and Getting Started Installation and Getting Started
=================================== ===================================
**Pythons**: Python 2.6,2.7,3.3,3.4,3.5,3.6 Jython, PyPy-2.3 **Pythons**: Python 2.7, 3.4, 3.5, 3.6, Jython, PyPy-2.3
**Platforms**: Unix/Posix and Windows **Platforms**: Unix/Posix and Windows
@ -9,8 +9,6 @@ Installation and Getting Started
**dependencies**: `py <http://pypi.python.org/pypi/py>`_, **dependencies**: `py <http://pypi.python.org/pypi/py>`_,
`colorama (Windows) <http://pypi.python.org/pypi/colorama>`_, `colorama (Windows) <http://pypi.python.org/pypi/colorama>`_,
`argparse (py26) <http://pypi.python.org/pypi/argparse>`_,
`ordereddict (py26) <http://pypi.python.org/pypi/ordereddict>`_.
**documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_ **documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_

View File

@ -57,7 +57,7 @@ Features
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box; - Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` 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 <http://plugincompat.herokuapp.com>`_ and thriving community; - Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;

192
doc/en/logging.rst Normal file
View File

@ -0,0 +1,192 @@
.. _logging:
Logging
-------
.. versionadded 3.3.0
.. note::
This feature is a drop-in replacement for the `pytest-catchlog
<https://pypi.org/project/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``

View File

@ -27,9 +27,6 @@ Here is a little annotated list for some popular plugins:
for `twisted <http://twistedmatrix.com>`_ apps, starting a reactor and for `twisted <http://twistedmatrix.com>`_ apps, starting a reactor and
processing deferreds from test functions. processing deferreds from test functions.
* `pytest-catchlog <http://pypi.python.org/pypi/pytest-catchlog>`_:
to capture and assert about messages from the logging module
* `pytest-cov <http://pypi.python.org/pypi/pytest-cov>`_: * `pytest-cov <http://pypi.python.org/pypi/pytest-cov>`_:
coverage reporting, compatible with distributed testing coverage reporting, compatible with distributed testing

View File

@ -58,6 +58,16 @@ by calling the ``pytest.skip(reason)`` function:
if not valid_config(): if not valid_config():
pytest.skip("unsupported configuration") 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
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 The imperative method is useful when it is not possible to evaluate the skip condition
during import time. during import time.
@ -68,11 +78,11 @@ during import time.
If you wish to skip something conditionally then you can use ``skipif`` instead. 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 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 import sys
@pytest.mark.skipif(sys.version_info < (3,3), @pytest.mark.skipif(sys.version_info < (3,6),
reason="requires python3.3") reason="requires python3.6")
def test_function(): def test_function():
... ...
@ -264,8 +274,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 As with skipif_ you can also mark your expectation of a failure
on a particular platform:: on a particular platform::
@pytest.mark.xfail(sys.version_info >= (3,3), @pytest.mark.xfail(sys.version_info >= (3,6),
reason="python3.3 api changes") reason="python3.6 api changes")
def test_function(): def test_function():
... ...

View File

@ -189,7 +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 for test output occurring after you exit the interactive PDB_ tracing session
and continue with the regular test run. and continue with the regular test run.
.. _durations: .. _durations:
Profiling test execution duration Profiling test execution duration

View File

@ -168,7 +168,20 @@ which works in a similar manner to :ref:`raises <assertraises>`::
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
warnings.warn("my warning", 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:: You can also call ``pytest.warns`` on a function or code string::

View File

@ -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 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 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 <pluggy._CallOutcome>` instance which encapsulates a result or
exception info. The yield point itself will thus typically not raise exception info. The yield point itself will thus typically not raise
exceptions (unless there are bugs). exceptions (unless there are bugs).
@ -517,7 +517,7 @@ Here is the order of execution:
Plugin1). Plugin1).
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield 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 <pluggy._CallOutcome>` instance which encapsulates
the result from calling the non-wrappers. Wrappers shall not modify the result. 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 It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
@ -714,7 +714,7 @@ Reference of objects involved in hooks
:members: :members:
:inherited-members: :inherited-members:
.. autoclass:: _pytest.vendored_packages.pluggy._CallOutcome() .. autoclass:: pluggy._CallOutcome()
:members: :members:
.. autofunction:: _pytest.config.get_plugin_manager() .. autofunction:: _pytest.config.get_plugin_manager()
@ -724,7 +724,7 @@ Reference of objects involved in hooks
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
.. autoclass:: _pytest.vendored_packages.pluggy.PluginManager() .. autoclass:: pluggy.PluginManager()
:members: :members:
.. currentmodule:: _pytest.pytester .. currentmodule:: _pytest.pytester

View File

@ -7,7 +7,7 @@ pytest: unit and functional testing with Python.
# else we are imported # else we are imported
from _pytest.config import ( from _pytest.config import (
main, UsageError, _preloadplugins, cmdline, main, UsageError, cmdline,
hookspec, hookimpl hookspec, hookimpl
) )
from _pytest.fixtures import fixture, yield_fixture from _pytest.fixtures import fixture, yield_fixture
@ -74,5 +74,4 @@ if __name__ == '__main__':
else: else:
from _pytest.compat import _setup_collect_fakemodule from _pytest.compat import _setup_collect_fakemodule
_preloadplugins() # to populate pytest.* namespace so help(pytest) works
_setup_collect_fakemodule() _setup_collect_fakemodule()

View File

@ -16,7 +16,7 @@ classifiers = [
'Topic :: Utilities', 'Topic :: Utilities',
] + [ ] + [
('Programming Language :: Python :: %s' % x) ('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 2.7 3 3.4 3.5 3.6'.split()
] ]
with open('README.rst') as fd: with open('README.rst') as fd:
@ -43,17 +43,25 @@ def has_environment_marker_support():
def main(): def main():
install_requires = ['py>=1.4.33', 'setuptools'] # pluggy is vendored in _pytest.vendored_packages
extras_require = {} 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:
install_requires.append('pluggy>=0.4.0,<0.5')
if has_environment_marker_support(): if has_environment_marker_support():
extras_require[':python_version=="2.6"'] = ['argparse', 'ordereddict'] extras_require[':python_version<"3.0"'] = ['funcsigs']
extras_require[':sys_platform=="win32"'] = ['colorama'] extras_require[':sys_platform=="win32"'] = ['colorama']
else: else:
if sys.version_info < (2, 7):
install_requires.append('argparse')
install_requires.append('ordereddict')
if sys.platform == 'win32': if sys.platform == 'win32':
install_requires.append('colorama') install_requires.append('colorama')
if sys.version_info < (3, 0):
install_requires.append('funcsigs')
setup( setup(
name='pytest', name='pytest',
@ -65,9 +73,11 @@ def main():
url='http://pytest.org', url='http://pytest.org',
license='MIT license', license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
author='Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others', author=(
entry_points={'console_scripts': 'Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, '
['pytest=pytest:main', 'py.test=pytest:main']}, 'Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others'),
entry_points={'console_scripts': [
'pytest=pytest:main', 'py.test=pytest:main']},
classifiers=classifiers, classifiers=classifiers,
keywords="test unittest", keywords="test unittest",
cmdclass={'test': PyTest}, cmdclass={'test': PyTest},
@ -75,7 +85,7 @@ def main():
setup_requires=['setuptools-scm'], setup_requires=['setuptools-scm'],
install_requires=install_requires, install_requires=install_requires,
extras_require=extras_require, extras_require=extras_require,
packages=['_pytest', '_pytest.assertion', '_pytest._code', '_pytest.vendored_packages'], packages=['_pytest', '_pytest.assertion', '_pytest._code'],
py_modules=['pytest'], py_modules=['pytest'],
zip_safe=False, zip_safe=False,
) )
@ -83,10 +93,13 @@ def main():
class PyTest(Command): class PyTest(Command):
user_options = [] user_options = []
def initialize_options(self): def initialize_options(self):
pass pass
def finalize_options(self): def finalize_options(self):
pass pass
def run(self): def run(self):
import subprocess import subprocess
PPATH = [x for x in os.environ.get('PYTHONPATH', '').split(':') if x] PPATH = [x for x in os.environ.get('PYTHONPATH', '').split(':') if x]

View File

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

View File

@ -344,7 +344,7 @@ class TestGeneralUsage(object):
Importing a module that didn't exist, even if the ImportError was Importing a module that didn't exist, even if the ImportError was
gracefully handled, would make our test crash. 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 ImportWarning: Not importing directory '...\not_a_package': missing __init__.py
""" """
testdir.mkdir('not_a_package') testdir.mkdir('not_a_package')

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys
import operator import operator
import _pytest import _pytest
import py import py
@ -345,9 +344,6 @@ def test_excinfo_no_sourcecode():
except ValueError: except ValueError:
excinfo = _pytest._code.ExceptionInfo() excinfo = _pytest._code.ExceptionInfo()
s = str(excinfo.traceback[-1]) s = str(excinfo.traceback[-1])
if py.std.sys.version_info < (2, 5):
assert s == " File '<string>':1 in ?\n ???\n"
else:
assert s == " File '<string>':1 in <module>\n ???\n" assert s == " File '<string>':1 in <module>\n ???\n"
@ -1244,9 +1240,6 @@ def test_no_recursion_index_on_recursion_error():
except: # noqa except: # noqa
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
exc_info = 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: else:
assert 0 assert 0

View File

@ -273,7 +273,6 @@ class TestSourceParsingAndCompiling(object):
assert getstatement(2, source).lines == source.lines[2:3] assert getstatement(2, source).lines == source.lines[2:3]
assert getstatement(3, source).lines == source.lines[3:4] 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): def test_getstatementrange_out_of_bounds_py3(self):
source = Source("if xxx:\n from .collections import something") source = Source("if xxx:\n from .collections import something")
r = source.getstatementrange(1) r = source.getstatementrange(1)
@ -283,7 +282,6 @@ class TestSourceParsingAndCompiling(object):
source = Source(":") source = Source(":")
pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) pytest.raises(SyntaxError, lambda: source.getstatementrange(0))
@pytest.mark.skipif("sys.version_info < (2,6)")
def test_compile_to_ast(self): def test_compile_to_ast(self):
import ast import ast
source = Source("x = 4") source = Source("x = 4")

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
import logging
logger = logging.getLogger(__name__)
sublogger = logging.getLogger(__name__ + '.baz')
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'')
assert caplog.records[0].levelname == 'INFO'
assert caplog.records[0].msg == u''
assert u'' in caplog.text
def test_clear(caplog):
logger.info(u'')
assert len(caplog.records)
caplog.clear()
assert not len(caplog.records)

View File

@ -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('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")
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('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")
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('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")
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('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")
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('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")
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('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")
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('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")
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

View File

@ -24,11 +24,8 @@ class MyDocTestRunner(doctest.DocTestRunner):
class TestApprox(object): class TestApprox(object):
def test_repr_string(self): 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'+-' plus_minus = u'\u00b1' if sys.version_info[0] > 2 else u'+-'
tol1, tol2, infr = '1.0e-06', '2.0e-06', 'inf' 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)) == '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( assert repr(approx([1.0, 2.0])) == 'approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])'.format(
pm=plus_minus, tol1=tol1, tol2=tol2) pm=plus_minus, tol1=tol1, tol2=tol2)
@ -375,9 +372,6 @@ class TestApprox(object):
assert [3] == [pytest.approx(4)] assert [3] == [pytest.approx(4)]
""") """)
expected = '4.0e-06' 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 = testdir.runpytest()
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
'*At index 0 diff: 3 != 4 * {0}'.format(expected), '*At index 0 diff: 3 != 4 * {0}'.format(expected),

View File

@ -164,13 +164,6 @@ class TestClass(object):
assert fix == 1 assert fix == 1
""") """)
result = testdir.runpytest() 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([ result.stdout.fnmatch_lines([
"*collected 2 items*", "*collected 2 items*",
"*2 passed in*", "*2 passed in*",
@ -819,10 +812,12 @@ class TestConftestCustomization(object):
def test_customized_pymakemodule_issue205_subdir(self, testdir): def test_customized_pymakemodule_issue205_subdir(self, testdir):
b = testdir.mkdir("a").mkdir("b") b = testdir.mkdir("a").mkdir("b")
b.join("conftest.py").write(_pytest._code.Source(""" b.join("conftest.py").write(_pytest._code.Source("""
def pytest_pycollect_makemodule(__multicall__): import pytest
mod = __multicall__.execute() @pytest.hookimpl(hookwrapper=True)
def pytest_pycollect_makemodule():
outcome = yield
mod = outcome.get_result()
mod.obj.hello = "world" mod.obj.hello = "world"
return mod
""")) """))
b.join("test_module.py").write(_pytest._code.Source(""" b.join("test_module.py").write(_pytest._code.Source("""
def test_hello(): def test_hello():
@ -839,7 +834,7 @@ class TestConftestCustomization(object):
def pytest_pycollect_makeitem(): def pytest_pycollect_makeitem():
outcome = yield outcome = yield
if outcome.excinfo is None: if outcome.excinfo is None:
result = outcome.result result = outcome.get_result()
if result: if result:
for func in result: for func in result:
func._some123 = "world" func._some123 = "world"

View File

@ -2,7 +2,6 @@ from textwrap import dedent
import _pytest._code import _pytest._code
import pytest import pytest
import sys
from _pytest.pytester import get_public_names from _pytest.pytester import get_public_names
from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupError
from _pytest import fixtures from _pytest import fixtures
@ -34,9 +33,6 @@ def test_getfuncargnames():
pass pass
assert fixtures.getfuncargnames(A().f) == ('arg1',) 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') assert fixtures.getfuncargnames(A.static, cls=A) == ('arg1', 'arg2')
@ -2826,7 +2822,7 @@ class TestShowFixtures(object):
import pytest import pytest
class TestClass: class TestClass:
@pytest.fixture @pytest.fixture
def fixture1(): def fixture1(self):
"""line1 """line1
line2 line2
indented line indented line

View File

@ -158,7 +158,7 @@ class TestMetafunc(object):
pass pass
metafunc = self.Metafunc(func) metafunc = self.Metafunc(func)
metafunc.parametrize("y", []) 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 test_parametrize_with_userobjects(self):
def func(x, y): def func(x, y):

Some files were not shown because too many files have changed in this diff Show More