From 693bb321f81905ab366bd25acbcc9aec7770e42b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 19 Sep 2015 01:03:05 +0200 Subject: [PATCH] make Test Outcomes inherit from BaseException instead of exception fixes #580 --- _pytest/fixtures.py | 6 +- _pytest/main.py | 3 +- _pytest/outcomes.py | 141 +++++++++++++++++++++++++++++++++++++++++ _pytest/python.py | 2 +- _pytest/python_api.py | 2 +- _pytest/recwarn.py | 4 +- _pytest/runner.py | 125 +----------------------------------- _pytest/skipping.py | 18 +----- _pytest/unittest.py | 4 +- changelog/580.feature | 1 + doc/en/builtin.rst | 10 +-- pytest.py | 3 +- testing/test_runner.py | 13 +++- 13 files changed, 176 insertions(+), 156 deletions(-) create mode 100644 _pytest/outcomes.py create mode 100644 changelog/580.feature diff --git a/_pytest/fixtures.py b/_pytest/fixtures.py index 1a6e245c75b..9b0b11b0aa3 100644 --- a/_pytest/fixtures.py +++ b/_pytest/fixtures.py @@ -16,7 +16,7 @@ getlocation, getfuncargnames, safe_getattr, ) -from _pytest.runner import fail +from _pytest.outcomes import fail, TEST_OUTCOME from _pytest.compat import FuncargnamesCompatAttr def pytest_sessionstart(session): @@ -120,7 +120,7 @@ def getfixturemarker(obj): exceptions.""" try: return getattr(obj, "_pytestfixturefunction", None) - except Exception: + except TEST_OUTCOME: # some objects raise errors like request (from flask import request) # we don't expect them to be fixture functions return None @@ -799,7 +799,7 @@ def pytest_fixture_setup(fixturedef, request): my_cache_key = request.param_index try: result = call_fixture_func(fixturefunc, request, kwargs) - except Exception: + except TEST_OUTCOME: fixturedef.cached_result = (None, my_cache_key, sys.exc_info()) raise fixturedef.cached_result = (result, my_cache_key, None) diff --git a/_pytest/main.py b/_pytest/main.py index 480810cc8f6..f5701bd8546 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -14,7 +14,8 @@ from UserDict import DictMixin as MappingMixin from _pytest.config import directory_arg, UsageError, hookimpl -from _pytest.runner import collect_one_node, exit +from _pytest.runner import collect_one_node +from _pytest.outcomes import exit tracebackcutdir = py.path.local(_pytest.__file__).dirpath() diff --git a/_pytest/outcomes.py b/_pytest/outcomes.py new file mode 100644 index 00000000000..57e36126011 --- /dev/null +++ b/_pytest/outcomes.py @@ -0,0 +1,141 @@ +""" +exception classes and constants handling test outcomes +as well as functions creating them +""" +from __future__ import absolute_import, division, print_function +import py +import sys + + +class OutcomeException(BaseException): + """ OutcomeException and its subclass instances indicate and + contain info about test and collection outcomes. + """ + def __init__(self, msg=None, pytrace=True): + BaseException.__init__(self, msg) + self.msg = msg + self.pytrace = pytrace + + def __repr__(self): + if self.msg: + val = self.msg + if isinstance(val, bytes): + val = py._builtin._totext(val, errors='replace') + return val + return "<%s instance>" %(self.__class__.__name__,) + __str__ = __repr__ + + +TEST_OUTCOME = (OutcomeException, Exception) + + +class Skipped(OutcomeException): + # XXX hackish: on 3k we fake to live in the builtins + # in order to have Skipped exception printing shorter/nicer + __module__ = 'builtins' + + def __init__(self, msg=None, pytrace=True, allow_module_level=False): + OutcomeException.__init__(self, msg=msg, pytrace=pytrace) + self.allow_module_level = allow_module_level + + +class Failed(OutcomeException): + """ raised from an explicit call to pytest.fail() """ + __module__ = 'builtins' + + +class Exit(KeyboardInterrupt): + """ raised for immediate program exits (no tracebacks/summaries)""" + def __init__(self, msg="unknown reason"): + self.msg = msg + KeyboardInterrupt.__init__(self, msg) + +# exposed helper methods + +def exit(msg): + """ exit testing process as if KeyboardInterrupt was triggered. """ + __tracebackhide__ = True + raise Exit(msg) + + +exit.Exception = Exit + + +def skip(msg=""): + """ skip an executing test with the given message. Note: it's usually + better to use the pytest.mark.skipif marker to declare a test to be + skipped under certain conditions like mismatching platforms or + dependencies. See the pytest_skipping plugin for details. + """ + __tracebackhide__ = True + raise Skipped(msg=msg) + + +skip.Exception = Skipped + + +def fail(msg="", pytrace=True): + """ explicitly fail an currently-executing test with the given Message. + + :arg pytrace: if false the msg represents the full failure information + and no python traceback will be reported. + """ + __tracebackhide__ = True + raise Failed(msg=msg, pytrace=pytrace) + + +fail.Exception = Failed + + + +class XFailed(fail.Exception): + """ raised from an explicit call to pytest.xfail() """ + + +def xfail(reason=""): + """ xfail an executing test or setup functions with the given reason.""" + __tracebackhide__ = True + raise XFailed(reason) + + +xfail.Exception = XFailed + + + +def importorskip(modname, minversion=None): + """ return imported module if it has at least "minversion" as its + __version__ attribute. If no minversion is specified the a skip + is only triggered if the module can not be imported. + """ + import warnings + __tracebackhide__ = True + compile(modname, '', 'eval') # to catch syntaxerrors + should_skip = False + + with warnings.catch_warnings(): + # make sure to ignore ImportWarnings that might happen because + # of existing directories with the same name we're trying to + # import but without a __init__.py file + warnings.simplefilter('ignore') + try: + __import__(modname) + except ImportError: + # Do not raise chained exception here(#1485) + should_skip = True + if should_skip: + raise Skipped("could not import %r" %(modname,), allow_module_level=True) + mod = sys.modules[modname] + if minversion is None: + return mod + verattr = getattr(mod, '__version__', None) + if minversion is not None: + try: + from pkg_resources import parse_version as pv + except ImportError: + raise Skipped("we have a required version for %r but can not import " + "pkg_resources to parse version strings." % (modname,), + allow_module_level=True) + if verattr is None or pv(verattr) < pv(minversion): + raise Skipped("module %r has __version__ %r, required is: %r" %( + modname, verattr, minversion), allow_module_level=True) + return mod diff --git a/_pytest/python.py b/_pytest/python.py index 1a313a59e3e..7c92cec01f8 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -22,7 +22,7 @@ get_real_func, getfslineno, safe_getattr, safe_str, getlocation, enum, ) -from _pytest.runner import fail +from _pytest.outcomes import fail cutdir1 = py.path.local(pluggy.__file__.rstrip("oc")) cutdir2 = py.path.local(_pytest.__file__).dirpath() diff --git a/_pytest/python_api.py b/_pytest/python_api.py index 1b27ba32758..1596d853ac7 100644 --- a/_pytest/python_api.py +++ b/_pytest/python_api.py @@ -4,7 +4,7 @@ import py from _pytest.compat import isclass -from _pytest.runner import fail +from _pytest.outcomes import fail import _pytest._code # builtin pytest.approx helper diff --git a/_pytest/recwarn.py b/_pytest/recwarn.py index 7ad6fef893d..faa484c266b 100644 --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -7,8 +7,9 @@ import py import sys import warnings -from _pytest.fixtures import yield_fixture +from _pytest.fixtures import yield_fixture +from _pytest.outcomes import fail @yield_fixture def recwarn(): @@ -190,7 +191,6 @@ def __exit__(self, *exc_info): if not any(issubclass(r.category, self.expected_warning) for r in self): __tracebackhide__ = True - from _pytest.runner import fail fail("DID NOT WARN. No warnings of type {0} was emitted. " "The list of emitted warnings is: {1}.".format( self.expected_warning, diff --git a/_pytest/runner.py b/_pytest/runner.py index fd0b549a9ab..d07c462c512 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -7,7 +7,7 @@ import py from _pytest._code.code import TerminalRepr, ExceptionInfo - +from _pytest.outcomes import skip, Skipped, TEST_OUTCOME # @@ -400,7 +400,7 @@ def _callfinalizers(self, colitem): fin = finalizers.pop() try: fin() - except Exception: + except TEST_OUTCOME: # XXX Only first exception will be seen by user, # ideally all should be reported. if exc is None: @@ -447,7 +447,7 @@ def prepare(self, colitem): self.stack.append(col) try: col.setup() - except Exception: + except TEST_OUTCOME: col._prepare_exc = sys.exc_info() raise @@ -459,122 +459,3 @@ def collect_one_node(collector): if call and check_interactive_exception(call, rep): ihook.pytest_exception_interact(node=collector, call=call, report=rep) return rep - - -# ============================================================= -# Test OutcomeExceptions and helpers for creating them. - - -class OutcomeException(Exception): - """ OutcomeException and its subclass instances indicate and - contain info about test and collection outcomes. - """ - def __init__(self, msg=None, pytrace=True): - Exception.__init__(self, msg) - self.msg = msg - self.pytrace = pytrace - - def __repr__(self): - if self.msg: - val = self.msg - if isinstance(val, bytes): - val = py._builtin._totext(val, errors='replace') - return val - return "<%s instance>" %(self.__class__.__name__,) - __str__ = __repr__ - -class Skipped(OutcomeException): - # XXX hackish: on 3k we fake to live in the builtins - # in order to have Skipped exception printing shorter/nicer - __module__ = 'builtins' - - def __init__(self, msg=None, pytrace=True, allow_module_level=False): - OutcomeException.__init__(self, msg=msg, pytrace=pytrace) - self.allow_module_level = allow_module_level - - -class Failed(OutcomeException): - """ raised from an explicit call to pytest.fail() """ - __module__ = 'builtins' - - -class Exit(KeyboardInterrupt): - """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, msg="unknown reason"): - self.msg = msg - KeyboardInterrupt.__init__(self, msg) - -# exposed helper methods - -def exit(msg): - """ exit testing process as if KeyboardInterrupt was triggered. """ - __tracebackhide__ = True - raise Exit(msg) - - -exit.Exception = Exit - - -def skip(msg=""): - """ skip an executing test with the given message. Note: it's usually - better to use the pytest.mark.skipif marker to declare a test to be - skipped under certain conditions like mismatching platforms or - dependencies. See the pytest_skipping plugin for details. - """ - __tracebackhide__ = True - raise Skipped(msg=msg) - - -skip.Exception = Skipped - - -def fail(msg="", pytrace=True): - """ explicitly fail an currently-executing test with the given Message. - - :arg pytrace: if false the msg represents the full failure information - and no python traceback will be reported. - """ - __tracebackhide__ = True - raise Failed(msg=msg, pytrace=pytrace) - - -fail.Exception = Failed - - -def importorskip(modname, minversion=None): - """ return imported module if it has at least "minversion" as its - __version__ attribute. If no minversion is specified the a skip - is only triggered if the module can not be imported. - """ - import warnings - __tracebackhide__ = True - compile(modname, '', 'eval') # to catch syntaxerrors - should_skip = False - - with warnings.catch_warnings(): - # make sure to ignore ImportWarnings that might happen because - # of existing directories with the same name we're trying to - # import but without a __init__.py file - warnings.simplefilter('ignore') - try: - __import__(modname) - except ImportError: - # Do not raise chained exception here(#1485) - should_skip = True - if should_skip: - raise Skipped("could not import %r" %(modname,), allow_module_level=True) - mod = sys.modules[modname] - if minversion is None: - return mod - verattr = getattr(mod, '__version__', None) - if minversion is not None: - try: - from pkg_resources import parse_version as pv - except ImportError: - raise Skipped("we have a required version for %r but can not import " - "pkg_resources to parse version strings." % (modname,), - allow_module_level=True) - if verattr is None or pv(verattr) < pv(minversion): - raise Skipped("module %r has __version__ %r, required is: %r" %( - modname, verattr, minversion), allow_module_level=True) - return mod diff --git a/_pytest/skipping.py b/_pytest/skipping.py index 5af1ca40404..be46ab2d824 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -8,7 +8,7 @@ import py from _pytest.config import hookimpl from _pytest.mark import MarkInfo, MarkDecorator -from _pytest.runner import fail, skip +from _pytest.outcomes import fail, skip, xfail, TEST_OUTCOME def pytest_addoption(parser): group = parser.getgroup("general") @@ -33,7 +33,7 @@ def pytest_configure(config): def nop(*args, **kwargs): pass - nop.Exception = XFailed + nop.Exception = xfail.Exception setattr(pytest, "xfail", nop) config.addinivalue_line("markers", @@ -59,18 +59,6 @@ def nop(*args, **kwargs): ) -class XFailed(fail.Exception): - """ raised from an explicit call to pytest.xfail() """ - - -def xfail(reason=""): - """ xfail an executing test or setup functions with the given reason.""" - __tracebackhide__ = True - raise XFailed(reason) - - -xfail.Exception = XFailed - class MarkEvaluator: def __init__(self, item, name): @@ -97,7 +85,7 @@ def invalidraise(self, exc): def istrue(self): try: return self._istrue() - except Exception: + except TEST_OUTCOME: self.exc = sys.exc_info() if isinstance(self.exc[1], SyntaxError): msg = [" " * (self.exc[1].offset + 4) + "^", ] diff --git a/_pytest/unittest.py b/_pytest/unittest.py index 0cf0f1726af..8909c50beae 100644 --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -7,9 +7,9 @@ # for transferring markers import _pytest._code from _pytest.config import hookimpl -from _pytest.runner import fail, skip +from _pytest.outcomes import fail, skip, xfail from _pytest.python import transfer_markers, Class, Module, Function -from _pytest.skipping import MarkEvaluator, xfail +from _pytest.skipping import MarkEvaluator def pytest_pycollect_makeitem(collector, name, obj): diff --git a/changelog/580.feature b/changelog/580.feature new file mode 100644 index 00000000000..5245c7341b1 --- /dev/null +++ b/changelog/580.feature @@ -0,0 +1 @@ +Exceptions raised by ``pytest.fail``, ``pytest.skip`` and ``pytest.xfail`` now subclass BaseException, making them harder to be caught unintentionally by normal code. \ No newline at end of file diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 26dbd44cb3e..8ef26a2db11 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -47,11 +47,11 @@ You can use the following functions in your test, fixture or setup functions to force a certain test outcome. Note that most often you can rather use declarative marks, see :ref:`skipping`. -.. autofunction:: _pytest.runner.fail -.. autofunction:: _pytest.runner.skip -.. autofunction:: _pytest.runner.importorskip -.. autofunction:: _pytest.skipping.xfail -.. autofunction:: _pytest.runner.exit +.. autofunction:: _pytest.outcomes.fail +.. autofunction:: _pytest.outcomes.skip +.. autofunction:: _pytest.outcomes.importorskip +.. autofunction:: _pytest.outcomes.xfail +.. autofunction:: _pytest.outcomes.exit Fixtures and requests ----------------------------------------------------- diff --git a/pytest.py b/pytest.py index da6b64910e0..1c914a6edff 100644 --- a/pytest.py +++ b/pytest.py @@ -16,9 +16,8 @@ from _pytest import __version__ from _pytest.debugging import pytestPDB as __pytestPDB from _pytest.recwarn import warns, deprecated_call -from _pytest.runner import fail, skip, importorskip, exit +from _pytest.outcomes import fail, skip, importorskip, exit, xfail from _pytest.mark import MARK_GEN as mark, param -from _pytest.skipping import xfail from _pytest.main import Item, Collector, File, Session from _pytest.fixtures import fillfixtures as _fillfuncargs from _pytest.python import ( diff --git a/testing/test_runner.py b/testing/test_runner.py index 51d430fc8bb..cbc5818b4c6 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -6,7 +6,7 @@ import py import pytest import sys -from _pytest import runner, main +from _pytest import runner, main, outcomes class TestSetupState(object): def test_setup(self, testdir): @@ -436,9 +436,18 @@ def pytest_runtest_teardown(item): def test_outcomeexception_exceptionattributes(): - outcome = runner.OutcomeException('test') + outcome = outcomes.OutcomeException('test') assert outcome.args[0] == outcome.msg + +def test_outcomeexception_passes_except_Exception(): + with pytest.raises(outcomes.OutcomeException): + try: + raise outcomes.OutcomeException('test') + except Exception: + pass + + def test_pytest_exit(): try: pytest.exit("hello")