Merge master into features

This commit is contained in:
Daniel Hahler 2019-04-27 00:43:00 +02:00
commit 7f519f8ab7
16 changed files with 127 additions and 50 deletions

View File

@ -1,8 +1,10 @@
<!--
Thanks for submitting an issue! Thanks for submitting an issue!
Here's a quick checklist in what to include: Here's a quick checklist for what to provide:
-->
- [ ] Include a detailed description of the bug or suggestion - [ ] a detailed description of the bug or suggestion
- [ ] `pip list` of the virtual environment you are using - [ ] output of `pip list` from the virtual environment you are using
- [ ] pytest and operating system versions - [ ] pytest and operating system versions
- [ ] Minimal example if possible - [ ] minimal example if possible

View File

@ -1,7 +1,9 @@
<!--
Thanks for submitting a PR, your contribution is really appreciated! Thanks for submitting a PR, your contribution is really appreciated!
Here's a quick checklist that should be present in PRs (you can delete this text from the final description, this is Here's a quick checklist that should be present in PRs.
just a guideline): (please delete this text from the final description, this is just a guideline)
-->
- [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details. - [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](https://github.com/pytest-dev/pytest/blob/master/changelog/README.rst) for details.
- [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes. - [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes.

View File

@ -105,6 +105,7 @@ Hugo van Kemenade
Hui Wang (coldnight) Hui Wang (coldnight)
Ian Bicking Ian Bicking
Ian Lesperance Ian Lesperance
Ilya Konstantinov
Ionuț Turturică Ionuț Turturică
Iwan Briquemont Iwan Briquemont
Jaap Broekhuizen Jaap Broekhuizen
@ -179,6 +180,7 @@ Nicholas Devenish
Nicholas Murphy Nicholas Murphy
Niclas Olofsson Niclas Olofsson
Nicolas Delaby Nicolas Delaby
Nikolay Kondratyev
Oleg Pidsadnyi Oleg Pidsadnyi
Oleg Sushchenko Oleg Sushchenko
Oliver Bestwalter Oliver Bestwalter

View File

@ -0,0 +1 @@
Fix crash caused by error in ``__repr__`` function with both ``showlocals`` and verbose output enabled.

View File

@ -0,0 +1 @@
Eliminate core dependency on 'terminal' plugin.

View File

@ -208,7 +208,7 @@ Here's a quick guide on how to skip tests in a module in different situations:
.. code-block:: python .. code-block:: python
pytestmark = pytest.mark.skipif(sys.platform == "win32", "tests for linux only") pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="tests for linux only")
3. Skip all tests in a module if some import is missing: 3. Skip all tests in a module if some import is missing:

View File

@ -3,7 +3,6 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
import inspect import inspect
import pprint
import re import re
import sys import sys
import traceback import traceback
@ -18,6 +17,7 @@ import six
from six import text_type from six import text_type
import _pytest import _pytest
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import _PY2 from _pytest.compat import _PY2
from _pytest.compat import _PY3 from _pytest.compat import _PY3
@ -614,14 +614,11 @@ class FormattedExcinfo(object):
source = source.deindent() source = source.deindent()
return source return source
def _saferepr(self, obj):
return saferepr(obj)
def repr_args(self, entry): def repr_args(self, entry):
if self.funcargs: if self.funcargs:
args = [] args = []
for argname, argvalue in entry.frame.getargs(var=True): for argname, argvalue in entry.frame.getargs(var=True):
args.append((argname, self._saferepr(argvalue))) args.append((argname, saferepr(argvalue)))
return ReprFuncArgs(args) return ReprFuncArgs(args)
def get_source(self, source, line_index=-1, excinfo=None, short=False): def get_source(self, source, line_index=-1, excinfo=None, short=False):
@ -674,9 +671,9 @@ class FormattedExcinfo(object):
# _repr() function, which is only reprlib.Repr in # _repr() function, which is only reprlib.Repr in
# disguise, so is very configurable. # disguise, so is very configurable.
if self.truncate_locals: if self.truncate_locals:
str_repr = self._saferepr(value) str_repr = saferepr(value)
else: else:
str_repr = pprint.pformat(value) str_repr = safeformat(value)
# if len(str_repr) < 70 or not isinstance(value, # if len(str_repr) < 70 or not isinstance(value,
# (list, tuple, dict)): # (list, tuple, dict)):
lines.append("%-10s = %s" % (name, str_repr)) lines.append("%-10s = %s" % (name, str_repr))

View File

@ -1,8 +1,26 @@
import sys import pprint
from six.moves import reprlib from six.moves import reprlib
def _call_and_format_exception(call, x, *args):
try:
# Try the vanilla repr and make sure that the result is a string
return call(x, *args)
except Exception as exc:
exc_name = type(exc).__name__
try:
exc_info = str(exc)
except Exception:
exc_info = "unknown"
return '<[%s("%s") raised in repr()] %s object at 0x%x>' % (
exc_name,
exc_info,
x.__class__.__name__,
id(x),
)
class SafeRepr(reprlib.Repr): class SafeRepr(reprlib.Repr):
"""subclass of repr.Repr that limits the resulting size of repr() """subclass of repr.Repr that limits the resulting size of repr()
and includes information on exceptions raised during the call. and includes information on exceptions raised during the call.
@ -33,28 +51,20 @@ class SafeRepr(reprlib.Repr):
return self._callhelper(repr, x) return self._callhelper(repr, x)
def _callhelper(self, call, x, *args): def _callhelper(self, call, x, *args):
try: s = _call_and_format_exception(call, x, *args)
# Try the vanilla repr and make sure that the result is a string if len(s) > self.maxsize:
s = call(x, *args) i = max(0, (self.maxsize - 3) // 2)
except Exception: j = max(0, self.maxsize - 3 - i)
cls, e, tb = sys.exc_info() s = s[:i] + "..." + s[len(s) - j :]
exc_name = getattr(cls, "__name__", "unknown") return s
try:
exc_info = str(e)
except Exception: def safeformat(obj):
exc_info = "unknown" """return a pretty printed string for the given object.
return '<[%s("%s") raised in repr()] %s object at 0x%x>' % ( Failing __repr__ functions of user instances will be represented
exc_name, with a short exception info.
exc_info, """
x.__class__.__name__, return _call_and_format_exception(pprint.pformat, obj)
id(x),
)
else:
if len(s) > self.maxsize:
i = max(0, (self.maxsize - 3) // 2)
j = max(0, self.maxsize - 3 - i)
s = s[:i] + "..." + s[len(s) - j :]
return s
def saferepr(obj, maxsize=240): def saferepr(obj, maxsize=240):

View File

@ -704,7 +704,7 @@ class Config(object):
return self return self
def notify_exception(self, excinfo, option=None): def notify_exception(self, excinfo, option=None):
if option and option.fulltrace: if option and getattr(option, "fulltrace", False):
style = "long" style = "long"
else: else:
style = "native" style = "native"

View File

@ -248,7 +248,7 @@ class Node(object):
if excinfo.errisinstance(fm.FixtureLookupError): if excinfo.errisinstance(fm.FixtureLookupError):
return excinfo.value.formatrepr() return excinfo.value.formatrepr()
tbfilter = True tbfilter = True
if self.config.option.fulltrace: if self.config.getoption("fulltrace", False):
style = "long" style = "long"
else: else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]]) tb = _pytest._code.Traceback([excinfo.traceback[-1]])
@ -260,12 +260,12 @@ class Node(object):
style = "long" style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it? # XXX should excinfo.getrepr record all data and toterminal() process it?
if style is None: if style is None:
if self.config.option.tbstyle == "short": if self.config.getoption("tbstyle", "auto") == "short":
style = "short" style = "short"
else: else:
style = "long" style = "long"
if self.config.option.verbose > 1: if self.config.getoption("verbose", 0) > 1:
truncate_locals = False truncate_locals = False
else: else:
truncate_locals = True truncate_locals = True
@ -279,7 +279,7 @@ class Node(object):
return excinfo.getrepr( return excinfo.getrepr(
funcargs=True, funcargs=True,
abspath=abspath, abspath=abspath,
showlocals=self.config.option.showlocals, showlocals=self.config.getoption("showlocals", False),
style=style, style=style,
tbfilter=tbfilter, tbfilter=tbfilter,
truncate_locals=truncate_locals, truncate_locals=truncate_locals,

View File

@ -820,7 +820,7 @@ class FunctionMixin(PyobjMixin):
self.obj = self._getobj() self.obj = self._getobj()
def _prunetraceback(self, excinfo): def _prunetraceback(self, excinfo):
if hasattr(self, "_obj") and not self.config.option.fulltrace: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code(get_real_func(self.obj)) code = _pytest._code.Code(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno path, firstlineno = code.path, code.firstlineno
traceback = excinfo.traceback traceback = excinfo.traceback
@ -835,14 +835,14 @@ class FunctionMixin(PyobjMixin):
excinfo.traceback = ntraceback.filter() excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to # issue364: mark all but first and last frames to
# only show a single-line message for each frame # only show a single-line message for each frame
if self.config.option.tbstyle == "auto": if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2: if len(excinfo.traceback) > 2:
for entry in excinfo.traceback[1:-1]: for entry in excinfo.traceback[1:-1]:
entry.set_repr_style("short") entry.set_repr_style("short")
def repr_failure(self, excinfo, outerr=None): def repr_failure(self, excinfo, outerr=None):
assert outerr is None, "XXX outerr usage is deprecated" assert outerr is None, "XXX outerr usage is deprecated"
style = self.config.option.tbstyle style = self.config.getoption("tbstyle", "auto")
if style == "auto": if style == "auto":
style = "long" style = "long"
return self._repr_failure_py(excinfo, style=style) return self._repr_failure_py(excinfo, style=style)

View File

@ -368,7 +368,7 @@ class TestReport(BaseReport):
longrepr = item.repr_failure(excinfo) longrepr = item.repr_failure(excinfo)
else: # exception in setup or teardown else: # exception in setup or teardown
longrepr = item._repr_failure_py( longrepr = item._repr_failure_py(
excinfo, style=item.config.option.tbstyle excinfo, style=item.config.getoption("tbstyle", "auto")
) )
for rwhen, key, content in item._report_sections: for rwhen, key, content in item._report_sections:
sections.append(("Captured %s %s" % (key, rwhen), content)) sections.append(("Captured %s %s" % (key, rwhen), content))

View File

@ -598,6 +598,35 @@ raise ValueError()
assert reprlocals.lines[2] == "y = 5" assert reprlocals.lines[2] == "y = 5"
assert reprlocals.lines[3] == "z = 7" assert reprlocals.lines[3] == "z = 7"
def test_repr_local_with_error(self):
class ObjWithErrorInRepr:
def __repr__(self):
raise NotImplementedError
p = FormattedExcinfo(showlocals=True, truncate_locals=False)
loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
reprlocals = p.repr_locals(loc)
assert reprlocals.lines
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
assert '[NotImplementedError("") raised in repr()]' in reprlocals.lines[1]
def test_repr_local_with_exception_in_class_property(self):
class ExceptionWithBrokenClass(Exception):
@property
def __class__(self):
raise TypeError("boom!")
class ObjWithErrorInRepr:
def __repr__(self):
raise ExceptionWithBrokenClass()
p = FormattedExcinfo(showlocals=True, truncate_locals=False)
loc = {"x": ObjWithErrorInRepr(), "__builtins__": {}}
reprlocals = p.repr_locals(loc)
assert reprlocals.lines
assert reprlocals.lines[0] == "__builtins__ = <builtins>"
assert '[ExceptionWithBrokenClass("") raised in repr()]' in reprlocals.lines[1]
def test_repr_local_truncated(self): def test_repr_local_truncated(self):
loc = {"l": [i for i in range(10)]} loc = {"l": [i for i in range(10)]}
p = FormattedExcinfo(showlocals=True) p = FormattedExcinfo(showlocals=True)

View File

@ -770,7 +770,7 @@ def test_notify_exception(testdir, capfd):
config = testdir.parseconfig() config = testdir.parseconfig()
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError) as excinfo:
raise ValueError(1) raise ValueError(1)
config.notify_exception(excinfo) config.notify_exception(excinfo, config.option)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert "ValueError" in err assert "ValueError" in err
@ -779,10 +779,17 @@ def test_notify_exception(testdir, capfd):
return True return True
config.pluginmanager.register(A()) config.pluginmanager.register(A())
config.notify_exception(excinfo) config.notify_exception(excinfo, config.option)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert not err assert not err
config = testdir.parseconfig("-p", "no:terminal")
with pytest.raises(ValueError) as excinfo:
raise ValueError(1)
config.notify_exception(excinfo, config.option)
out, err = capfd.readouterr()
assert "ValueError" in err
def test_load_initial_conftest_last_ordering(testdir, _config_for_test): def test_load_initial_conftest_last_ordering(testdir, _config_for_test):
pm = _config_for_test.pluginmanager pm = _config_for_test.pluginmanager

View File

@ -134,6 +134,30 @@ class SessionTests(object):
!= -1 != -1
) )
def test_broken_repr_with_showlocals_verbose(self, testdir):
p = testdir.makepyfile(
"""
class ObjWithErrorInRepr:
def __repr__(self):
raise NotImplementedError
def test_repr_error():
x = ObjWithErrorInRepr()
assert x == "value"
"""
)
reprec = testdir.inline_run("--showlocals", "-vv", p)
passed, skipped, failed = reprec.listoutcomes()
assert (len(passed), len(skipped), len(failed)) == (0, 0, 1)
entries = failed[0].longrepr.reprtraceback.reprentries
assert len(entries) == 1
repr_locals = entries[0].reprlocals
assert repr_locals.lines
assert len(repr_locals.lines) == 1
assert repr_locals.lines[0].startswith(
'x = <[NotImplementedError("") raised in repr()] ObjWithErrorInRepr'
)
def test_skip_file_by_conftest(self, testdir): def test_skip_file_by_conftest(self, testdir):
testdir.makepyfile( testdir.makepyfile(
conftest=""" conftest="""

View File

@ -73,7 +73,8 @@ commands = pre-commit run --all-files --show-diff-on-failure
[testenv:docs] [testenv:docs]
basepython = python3 basepython = python3
usedevelop = True # broken due to pip 19.1 (#5167)
# usedevelop = True
changedir = doc/en changedir = doc/en
deps = -r{toxinidir}/doc/en/requirements.txt deps = -r{toxinidir}/doc/en/requirements.txt
@ -127,7 +128,8 @@ commands =
[testenv:release] [testenv:release]
decription = do a release, required posarg of the version number decription = do a release, required posarg of the version number
basepython = python3.6 basepython = python3.6
usedevelop = True # broken due to pip 19.1 (#5167)
# usedevelop = True
passenv = * passenv = *
deps = deps =
colorama colorama