Deprecate `raises(..., 'code(as_a_string)')` / `warns(..., 'code(as_a_string)')

This commit is contained in:
Anthony Sottile 2018-11-22 10:05:10 -08:00
parent 5cf69fae7d
commit 1bba0a9714
26 changed files with 140 additions and 96 deletions

View File

@ -0,0 +1 @@
Deprecate ``raises(..., 'code(as_a_string)')`` and ``warns(..., 'code(as_a_string)')``. See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec

View File

@ -100,10 +100,9 @@ If you want to write test code that works on Python 2.4 as well,
you may also use two other ways to test for an expected exception::
pytest.raises(ExpectedException, func, *args, **kwargs)
pytest.raises(ExpectedException, "func(*args, **kwargs)")
both of which execute the specified function with args and kwargs and
asserts that the given ``ExpectedException`` is raised. The reporter will
which will execute the specified function with args and kwargs and
assert that the given ``ExpectedException`` is raised. The reporter will
provide you with helpful output in case of failures such as *no
exception* or *wrong exception*.

View File

@ -14,6 +14,41 @@ Below is a complete list of all pytest features which are considered deprecated.
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
:ref:`standard warning filters <warnings>`.
.. _raises-warns-exec:
``raises`` / ``warns`` with a string as the second argument
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 4.1
Use the context manager form of these instead. When necessary, invoke ``exec``
directly.
Example:
.. code-block:: python
pytest.raises(ZeroDivisionError, "1 / 0")
pytest.raises(SyntaxError, "a $ b")
pytest.warns(DeprecationWarning, "my_function()")
pytest.warns(SyntaxWarning, "assert(1, 2)")
Becomes:
.. code-block:: python
with pytest.raises(ZeroDivisionError):
1 / 0
with pytest.raises(SyntaxError):
exec("a $ b") # exec is required for invalid syntax
with pytest.warns(DeprecationWarning):
my_function()
with pytest.warns(SyntaxWarning):
exec("assert(1, 2)") # exec is used to avoid a top-level warning
Internal classes accessed through ``Node``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -165,11 +165,11 @@ def globf(x):
class TestRaises(object):
def test_raises(self):
s = "qwe" # NOQA
raises(TypeError, "int(s)")
s = "qwe"
raises(TypeError, int, s)
def test_raises_doesnt(self):
raises(IOError, "int('3')")
raises(IOError, int, "3")
def test_raise(self):
raise ValueError("demo error")

View File

@ -388,7 +388,8 @@ parametrizer`_ but in a lot less code::
assert a == b
def test_zerodivision(self, a, b):
pytest.raises(ZeroDivisionError, "a/b")
with pytest.raises(ZeroDivisionError):
a / b
Our test generator looks up a class-level definition which specifies which
argument sets to use for each test function. Let's run it:

View File

@ -92,6 +92,15 @@ NODE_WARN = RemovedInPytest4Warning(
"Node.warn(code, message) form has been deprecated, use Node.warn(warning_instance) instead."
)
RAISES_EXEC = PytestDeprecationWarning(
"raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n"
"See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec"
)
WARNS_EXEC = PytestDeprecationWarning(
"warns(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly.\n\n"
"See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec"
)
RECORD_XML_PROPERTY = RemovedInPytest4Warning(
'Fixture renamed from "record_xml_property" to "record_property" as user '
"properties are now available to all reporters.\n"

View File

@ -1,6 +1,9 @@
from __future__ import absolute_import
import math
import pprint
import sys
import warnings
from decimal import Decimal
from numbers import Number
@ -14,6 +17,7 @@ from _pytest.compat import isclass
from _pytest.compat import Mapping
from _pytest.compat import Sequence
from _pytest.compat import STRING_TYPES
from _pytest.deprecated import RAISES_EXEC
from _pytest.outcomes import fail
BASE_TYPE = (type, STRING_TYPES)
@ -604,9 +608,9 @@ def raises(expected_exception, *args, **kwargs):
>>> with raises(ValueError, match=r'must be \d+$'):
... raise ValueError("value must be 42")
**Legacy forms**
**Legacy form**
The forms below are fully supported but are discouraged for new code because the
The form below is fully supported but discouraged for new code because the
context manager form is regarded as more readable and less error-prone.
It is possible to specify a callable by passing a to-be-called lambda::
@ -623,14 +627,6 @@ def raises(expected_exception, *args, **kwargs):
>>> raises(ZeroDivisionError, f, x=0)
<ExceptionInfo ...>
It is also possible to pass a string to be evaluated at runtime::
>>> raises(ZeroDivisionError, "f(0)")
<ExceptionInfo ...>
The string will be evaluated using the same ``locals()`` and ``globals()``
at the moment of the ``raises`` call.
.. currentmodule:: _pytest._code
Consult the API of ``excinfo`` objects: :class:`ExceptionInfo`.
@ -672,6 +668,7 @@ def raises(expected_exception, *args, **kwargs):
raise TypeError(msg)
return RaisesContext(expected_exception, message, match_expr)
elif isinstance(args[0], str):
warnings.warn(RAISES_EXEC, stacklevel=2)
code, = args
assert isinstance(code, str)
frame = sys._getframe(1)

View File

@ -11,6 +11,7 @@ import warnings
import six
import _pytest._code
from _pytest.deprecated import WARNS_EXEC
from _pytest.fixtures import yield_fixture
from _pytest.outcomes import fail
@ -89,6 +90,7 @@ def warns(expected_warning, *args, **kwargs):
match_expr = kwargs.pop("match")
return WarningsChecker(expected_warning, match_expr=match_expr)
elif isinstance(args[0], str):
warnings.warn(WARNS_EXEC, stacklevel=2)
code, = args
assert isinstance(code, str)
frame = sys._getframe(1)

View File

@ -37,7 +37,7 @@ def test_code_with_class():
class A(object):
pass
pytest.raises(TypeError, "_pytest._code.Code(A)")
pytest.raises(TypeError, _pytest._code.Code, A)
def x():

View File

@ -180,7 +180,8 @@ class TestTraceback_f_g_h(object):
def test_traceback_cut_excludepath(self, testdir):
p = testdir.makepyfile("def f(): raise ValueError")
excinfo = pytest.raises(ValueError, "p.pyimport().f()")
with pytest.raises(ValueError) as excinfo:
p.pyimport().f()
basedir = py.path.local(pytest.__file__).dirpath()
newtraceback = excinfo.traceback.cut(excludepath=basedir)
for x in newtraceback:
@ -336,7 +337,8 @@ class TestTraceback_f_g_h(object):
def test_excinfo_exconly():
excinfo = pytest.raises(ValueError, h)
assert excinfo.exconly().startswith("ValueError")
excinfo = pytest.raises(ValueError, "raise ValueError('hello\\nworld')")
with pytest.raises(ValueError) as excinfo:
raise ValueError("hello\nworld")
msg = excinfo.exconly(tryshort=True)
assert msg.startswith("ValueError")
assert msg.endswith("world")

View File

@ -6,6 +6,7 @@ from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import ast
import inspect
import sys
@ -14,7 +15,6 @@ import six
import _pytest._code
import pytest
from _pytest._code import Source
from _pytest._code.source import ast
astonly = pytest.mark.nothing
@ -306,8 +306,6 @@ class TestSourceParsingAndCompiling(object):
pytest.raises(SyntaxError, lambda: source.getstatementrange(0))
def test_compile_to_ast(self):
import ast
source = Source("x = 4")
mod = source.compile(flag=ast.PyCF_ONLY_AST)
assert isinstance(mod, ast.Module)
@ -317,10 +315,9 @@ class TestSourceParsingAndCompiling(object):
co = self.source.compile()
six.exec_(co, globals())
f(7)
excinfo = pytest.raises(AssertionError, "f(6)")
excinfo = pytest.raises(AssertionError, f, 6)
frame = excinfo.traceback[-1].frame
stmt = frame.code.fullsource.getstatement(frame.lineno)
# print "block", str(block)
assert str(stmt).strip().startswith("assert")
@pytest.mark.parametrize("name", ["", None, "my"])
@ -361,17 +358,13 @@ def test_getline_finally():
def c():
pass
excinfo = pytest.raises(
TypeError,
"""
with pytest.raises(TypeError) as excinfo:
teardown = None
try:
c(1)
finally:
if teardown:
teardown()
""",
)
source = excinfo.traceback[-1].statement
assert str(source).strip() == "c(1)"

View File

@ -3,4 +3,4 @@ import pytest
@pytest.fixture
def arg2(request):
pytest.raises(Exception, "request.getfixturevalue('arg1')")
pytest.raises(Exception, request.getfixturevalue, "arg1")

View File

@ -325,7 +325,7 @@ class TestGenerator(object):
assert len(colitems) == 1
gencol = colitems[0]
assert isinstance(gencol, pytest.Generator)
pytest.raises(ValueError, "gencol.collect()")
pytest.raises(ValueError, gencol.collect)
def test_generative_methods_with_explicit_names(self, testdir):
modcol = testdir.getmodulecol(
@ -1103,7 +1103,8 @@ def test_modulecol_roundtrip(testdir):
class TestTracebackCutting(object):
def test_skip_simple(self):
excinfo = pytest.raises(pytest.skip.Exception, 'pytest.skip("xxx")')
with pytest.raises(pytest.skip.Exception) as excinfo:
pytest.skip("xxx")
assert excinfo.traceback[-1].frame.code.name == "skip"
assert excinfo.traceback[-1].ishidden()

View File

@ -906,7 +906,8 @@ class TestRequestMarking(object):
assert "skipif" not in item1.keywords
req1.applymarker(pytest.mark.skipif)
assert "skipif" in item1.keywords
pytest.raises(ValueError, "req1.applymarker(42)")
with pytest.raises(ValueError):
req1.applymarker(42)
def test_accesskeywords(self, testdir):
testdir.makepyfile(

View File

@ -70,11 +70,11 @@ class TestMetafunc(object):
pass
metafunc = self.Metafunc(func)
pytest.raises(ValueError, "metafunc.addcall(id=None)")
pytest.raises(ValueError, metafunc.addcall, id=None)
metafunc.addcall(id=1)
pytest.raises(ValueError, "metafunc.addcall(id=1)")
pytest.raises(ValueError, "metafunc.addcall(id='1')")
pytest.raises(ValueError, metafunc.addcall, id=1)
pytest.raises(ValueError, metafunc.addcall, id="1")
metafunc.addcall(id=2)
assert len(metafunc._calls) == 2
assert metafunc._calls[0].id == "1"
@ -108,7 +108,7 @@ class TestMetafunc(object):
metafunc.addcall(funcargs={"x": 2})
metafunc.addcall(funcargs={"x": 3})
pytest.raises(pytest.fail.Exception, "metafunc.addcall({'xyz': 0})")
pytest.raises(pytest.fail.Exception, metafunc.addcall, {"xyz": 0})
assert len(metafunc._calls) == 2
assert metafunc._calls[0].funcargs == {"x": 2}
assert metafunc._calls[1].funcargs == {"x": 3}

View File

@ -4,25 +4,32 @@ import six
import pytest
from _pytest.outcomes import Failed
from _pytest.warning_types import PytestDeprecationWarning
class TestRaises(object):
def test_raises(self):
source = "int('qwe')"
with pytest.warns(PytestDeprecationWarning):
excinfo = pytest.raises(ValueError, source)
code = excinfo.traceback[-1].frame.code
s = str(code.fullsource)
assert s == source
def test_raises_exec(self):
with pytest.warns(PytestDeprecationWarning) as warninfo:
pytest.raises(ValueError, "a,x = []")
assert warninfo[0].filename == __file__
def test_raises_exec_correct_filename(self):
with pytest.warns(PytestDeprecationWarning):
excinfo = pytest.raises(ValueError, 'int("s")')
assert __file__ in excinfo.traceback[-1].path
def test_raises_syntax_error(self):
with pytest.warns(PytestDeprecationWarning) as warninfo:
pytest.raises(SyntaxError, "qwe qwe qwe")
assert warninfo[0].filename == __file__
def test_raises_function(self):
pytest.raises(ValueError, int, "hello")

View File

@ -87,7 +87,7 @@ class TestCaptureManager(object):
try:
capman = CaptureManager("fd")
capman.start_global_capturing()
pytest.raises(AssertionError, "capman.start_global_capturing()")
pytest.raises(AssertionError, capman.start_global_capturing)
capman.stop_global_capturing()
finally:
capouter.stop_capturing()
@ -798,10 +798,10 @@ class TestCaptureIO(object):
f = capture.CaptureIO()
if sys.version_info >= (3, 0):
f.write("\u00f6")
pytest.raises(TypeError, "f.write(bytes('hello', 'UTF-8'))")
pytest.raises(TypeError, f.write, b"hello")
else:
f.write(text_type("\u00f6", "UTF-8"))
f.write("hello") # bytes
f.write(u"\u00f6")
f.write(b"hello")
s = f.getvalue()
f.close()
assert isinstance(s, text_type)
@ -1149,7 +1149,7 @@ class TestStdCapture(object):
print("XXX which indicates an error in the underlying capturing")
print("XXX mechanisms")
with self.getcapture():
pytest.raises(IOError, "sys.stdin.read()")
pytest.raises(IOError, sys.stdin.read)
class TestStdCaptureFD(TestStdCapture):

View File

@ -194,7 +194,7 @@ class TestConfigAPI(object):
config = testdir.parseconfig("--hello=this")
for x in ("hello", "--hello", "-X"):
assert config.getoption(x) == "this"
pytest.raises(ValueError, "config.getoption('qweqwe')")
pytest.raises(ValueError, config.getoption, "qweqwe")
@pytest.mark.skipif("sys.version_info[0] < 3")
def test_config_getoption_unicode(self, testdir):
@ -211,7 +211,7 @@ class TestConfigAPI(object):
def test_config_getvalueorskip(self, testdir):
config = testdir.parseconfig()
pytest.raises(pytest.skip.Exception, "config.getvalueorskip('hello')")
pytest.raises(pytest.skip.Exception, config.getvalueorskip, "hello")
verbose = config.getvalueorskip("verbose")
assert verbose == config.option.verbose
@ -723,7 +723,8 @@ def test_config_in_subdirectory_colon_command_line_issue2148(testdir):
def test_notify_exception(testdir, capfd):
config = testdir.parseconfig()
excinfo = pytest.raises(ValueError, "raise ValueError(1)")
with pytest.raises(ValueError) as excinfo:
raise ValueError(1)
config.notify_exception(excinfo)
out, err = capfd.readouterr()
assert "ValueError" in err

View File

@ -27,7 +27,7 @@ def test_setattr():
x = 1
monkeypatch = MonkeyPatch()
pytest.raises(AttributeError, "monkeypatch.setattr(A, 'notexists', 2)")
pytest.raises(AttributeError, monkeypatch.setattr, A, "notexists", 2)
monkeypatch.setattr(A, "y", 2, raising=False)
assert A.y == 2
monkeypatch.undo()
@ -99,7 +99,7 @@ def test_delattr():
monkeypatch = MonkeyPatch()
monkeypatch.delattr(A, "x")
pytest.raises(AttributeError, "monkeypatch.delattr(A, 'y')")
pytest.raises(AttributeError, monkeypatch.delattr, A, "y")
monkeypatch.delattr(A, "y", raising=False)
monkeypatch.setattr(A, "x", 5, raising=False)
assert A.x == 5
@ -156,7 +156,7 @@ def test_delitem():
monkeypatch.delitem(d, "x")
assert "x" not in d
monkeypatch.delitem(d, "y", raising=False)
pytest.raises(KeyError, "monkeypatch.delitem(d, 'y')")
pytest.raises(KeyError, monkeypatch.delitem, d, "y")
assert not d
monkeypatch.setitem(d, "y", 1700)
assert d["y"] == 1700
@ -182,7 +182,7 @@ def test_delenv():
name = "xyz1234"
assert name not in os.environ
monkeypatch = MonkeyPatch()
pytest.raises(KeyError, "monkeypatch.delenv(%r, raising=True)" % name)
pytest.raises(KeyError, monkeypatch.delenv, name, raising=True)
monkeypatch.delenv(name, raising=False)
monkeypatch.undo()
os.environ[name] = "1"

View File

@ -100,12 +100,8 @@ class TestParser(object):
def test_group_shortopt_lowercase(self, parser):
group = parser.getgroup("hello")
pytest.raises(
ValueError,
"""
with pytest.raises(ValueError):
group.addoption("-x", action="store_true")
""",
)
assert len(group.options) == 0
group._addoption("-x", action="store_true")
assert len(group.options) == 1

View File

@ -196,7 +196,7 @@ class TestPytestPluginManager(object):
assert pm.is_registered(mod)
values = pm.get_plugins()
assert mod in values
pytest.raises(ValueError, "pm.register(mod)")
pytest.raises(ValueError, pm.register, mod)
pytest.raises(ValueError, lambda: pm.register(mod))
# assert not pm.is_registered(mod2)
assert pm.get_plugins() == values
@ -284,8 +284,8 @@ class TestPytestPluginManager(object):
result.stdout.fnmatch_lines(["*1 passed*"])
def test_import_plugin_importname(self, testdir, pytestpm):
pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")')
pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwx.y")')
pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwx.y")
testdir.syspathinsert()
pluginname = "pytest_hello"
@ -301,8 +301,8 @@ class TestPytestPluginManager(object):
assert plugin2 is plugin1
def test_import_plugin_dotted_name(self, testdir, pytestpm):
pytest.raises(ImportError, 'pytestpm.import_plugin("qweqwex.y")')
pytest.raises(ImportError, 'pytestpm.import_plugin("pytest_qweqwex.y")')
pytest.raises(ImportError, pytestpm.import_plugin, "qweqwex.y")
pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y")
testdir.syspathinsert()
testdir.mkpydir("pkg").join("plug.py").write("x=3")

View File

@ -71,7 +71,7 @@ def test_make_hook_recorder(testdir):
recorder.unregister()
recorder.clear()
recorder.hook.pytest_runtest_logreport(report=rep)
pytest.raises(ValueError, "recorder.getfailures()")
pytest.raises(ValueError, recorder.getfailures)
def test_parseconfig(testdir):
@ -174,7 +174,7 @@ def test_hookrecorder_basic(holder):
call = rec.popcall("pytest_xyz")
assert call.arg == 123
assert call._name == "pytest_xyz"
pytest.raises(pytest.fail.Exception, "rec.popcall('abc')")
pytest.raises(pytest.fail.Exception, rec.popcall, "abc")
pm.hook.pytest_xyz_noarg()
call = rec.popcall("pytest_xyz_noarg")
assert call._name == "pytest_xyz_noarg"

View File

@ -7,6 +7,7 @@ import warnings
import pytest
from _pytest.recwarn import WarningsRecorder
from _pytest.warning_types import PytestDeprecationWarning
def test_recwarn_stacklevel(recwarn):
@ -44,7 +45,7 @@ class TestWarningsRecorderChecker(object):
rec.clear()
assert len(rec.list) == 0
assert values is rec.list
pytest.raises(AssertionError, "rec.pop()")
pytest.raises(AssertionError, rec.pop)
@pytest.mark.issue(4243)
def test_warn_stacklevel(self):
@ -214,9 +215,17 @@ class TestWarns(object):
source1 = "warnings.warn('w1', RuntimeWarning)"
source2 = "warnings.warn('w2', RuntimeWarning)"
source3 = "warnings.warn('w3', RuntimeWarning)"
with pytest.warns(PytestDeprecationWarning) as warninfo: # yo dawg
pytest.warns(RuntimeWarning, source1)
pytest.raises(pytest.fail.Exception, lambda: pytest.warns(UserWarning, source2))
pytest.raises(
pytest.fail.Exception, lambda: pytest.warns(UserWarning, source2)
)
pytest.warns(RuntimeWarning, source3)
assert len(warninfo) == 3
for w in warninfo:
assert w.filename == __file__
msg, = w.message.args
assert msg.startswith("warns(..., 'code(as_a_string)') is deprecated")
def test_function(self):
pytest.warns(

View File

@ -700,17 +700,13 @@ def test_importorskip(monkeypatch):
# check that importorskip reports the actual call
# in this test the test_runner.py file
assert path.purebasename == "test_runner"
pytest.raises(SyntaxError, "pytest.importorskip('x y z')")
pytest.raises(SyntaxError, "pytest.importorskip('x=y')")
pytest.raises(SyntaxError, pytest.importorskip, "x y z")
pytest.raises(SyntaxError, pytest.importorskip, "x=y")
mod = types.ModuleType("hello123")
mod.__version__ = "1.3"
monkeypatch.setitem(sys.modules, "hello123", mod)
pytest.raises(
pytest.skip.Exception,
"""
with pytest.raises(pytest.skip.Exception):
pytest.importorskip("hello123", minversion="1.3.1")
""",
)
mod2 = pytest.importorskip("hello123", minversion="1.3")
assert mod2 == mod
except pytest.skip.Exception:
@ -730,11 +726,8 @@ def test_importorskip_dev_module(monkeypatch):
monkeypatch.setitem(sys.modules, "mockmodule", mod)
mod2 = pytest.importorskip("mockmodule", minversion="0.12.0")
assert mod2 == mod
pytest.raises(
pytest.skip.Exception,
"""
pytest.importorskip('mockmodule1', minversion='0.14.0')""",
)
with pytest.raises(pytest.skip.Exception):
pytest.importorskip("mockmodule1", minversion="0.14.0")
except pytest.skip.Exception:
print(_pytest._code.ExceptionInfo.from_current())
pytest.fail("spurious skip")

View File

@ -243,12 +243,8 @@ class TestNewSession(SessionTests):
def test_plugin_specify(testdir):
pytest.raises(
ImportError,
"""
with pytest.raises(ImportError):
testdir.parseconfig("-p", "nqweotexistent")
""",
)
# pytest.raises(ImportError,
# "config.do_configure(config)"
# )

View File

@ -105,7 +105,8 @@ class TestTerminal(object):
def test_internalerror(self, testdir, linecomp):
modcol = testdir.getmodulecol("def test_one(): pass")
rep = TerminalReporter(modcol.config, file=linecomp.stringio)
excinfo = pytest.raises(ValueError, "raise ValueError('hello')")
with pytest.raises(ValueError) as excinfo:
raise ValueError("hello")
rep.pytest_internalerror(excinfo.getrepr())
linecomp.assert_contains_lines(["INTERNALERROR> *ValueError*hello*"])