From d5e463605e3a8e746562121e72f11906b1f3909d Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 27 Apr 2010 21:13:09 +0200 Subject: [PATCH] * properly expose and document runtest-protocol related Exceptions and move all definitions to the runner plugin for now. * also move EXIT codes to session.py, obsoleting outcome.py alltogether. --HG-- branch : trunk --- CHANGELOG | 2 + py/__init__.py | 5 - py/_plugin/pytest_pdb.py | 4 +- py/_plugin/pytest_runner.py | 144 +++++++++++++++++++++++++- py/_test/outcome.py | 136 ------------------------ py/_test/pluginmanager.py | 3 +- py/_test/session.py | 18 ++-- testing/plugin/test_pytest_doctest.py | 2 - testing/plugin/test_pytest_runner.py | 8 +- testing/root/test_py_imports.py | 3 +- testing/test_config.py | 7 +- testing/test_deprecated_api.py | 5 +- testing/test_outcome.py | 13 ++- testing/test_pycollect.py | 3 +- 14 files changed, 175 insertions(+), 178 deletions(-) delete mode 100644 py/_test/outcome.py diff --git a/CHANGELOG b/CHANGELOG index 42c895399..2ee69f4dc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,8 @@ Changes between 1.2.1 and 1.2.2 (release pending) - added links to the new capturelog and coverage plugins - (issue87) fix unboundlocal error in assertionold code - (issue86) improve documentation for looponfailing +- expose some internal test running exceptions under py.test.exc.* + and shift raises/importorskip etc. helper definitions to runner plugin . - ship distribute_setup.py version 0.6.10 diff --git a/py/__init__.py b/py/__init__.py index 363ca6f27..aa8c881fc 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -38,11 +38,6 @@ py.apipkg.initpkg(__name__, dict( # helpers for use from test functions or collectors '__onfirstaccess__' : '._test.config:onpytestaccess', '__doc__' : '._test:__doc__', - 'raises' : '._test.outcome:raises', - 'skip' : '._test.outcome:skip', - 'importorskip' : '._test.outcome:importorskip', - 'fail' : '._test.outcome:fail', - 'exit' : '._test.outcome:exit', # configuration/initialization related test api 'config' : '._test.config:config_per_process', 'ensuretemp' : '._test.config:ensuretemp', diff --git a/py/_plugin/pytest_pdb.py b/py/_plugin/pytest_pdb.py index 1c5884920..e2a3bb3e2 100644 --- a/py/_plugin/pytest_pdb.py +++ b/py/_plugin/pytest_pdb.py @@ -3,7 +3,6 @@ interactive debugging with the Python Debugger. """ import py import pdb, sys, linecache -from py._test.outcome import Skipped def pytest_addoption(parser): group = parser.getgroup("general") @@ -17,7 +16,8 @@ def pytest_configure(config): class PdbInvoke: def pytest_runtest_makereport(self, item, call): - if call.excinfo and not call.excinfo.errisinstance(Skipped): + if call.excinfo and not \ + call.excinfo.errisinstance(py.test.exc.Skipped): # play well with capturing, slightly hackish capman = item.config.pluginmanager.getplugin('capturemanager') capman.suspendcapture() diff --git a/py/_plugin/pytest_runner.py b/py/_plugin/pytest_runner.py index 11b068a0c..cf0f9a301 100644 --- a/py/_plugin/pytest_runner.py +++ b/py/_plugin/pytest_runner.py @@ -3,7 +3,23 @@ collect and run test items and create reports. """ import py, sys -from py._test.outcome import Skipped + +def pytest_namespace(): + class exc: + """ namespace holding py.test runner exceptions. """ + Skipped = Skipped + ExceptionFailure = ExceptionFailure + Failed = Failed + Exit = Exit + + return { + 'exc' : exc, + 'raises' : raises, + 'skip' : skip, + 'importorskip' : importorskip, + 'fail' : fail, + 'exit' : exit, + } # # pytest plugin hooks @@ -141,7 +157,7 @@ class ItemTestReport(BaseReport): self.failed = True shortrepr = "?" longrepr = excinfo - elif excinfo.errisinstance(Skipped): + elif excinfo.errisinstance(py.test.exc.Skipped): self.skipped = True shortrepr = "s" longrepr = self.item._repr_failure_py(excinfo) @@ -180,7 +196,7 @@ class CollectReport(BaseReport): self.result = result else: self.longrepr = self.collector._repr_failure_py(excinfo) - if excinfo.errisinstance(Skipped): + if excinfo.errisinstance(py.test.exc.Skipped): self.skipped = True self.reason = str(excinfo.value) else: @@ -259,3 +275,125 @@ class SetupState(object): except Exception: col._prepare_exc = sys.exc_info() raise + +# ============================================================= +# 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, excinfo=None): + self.msg = msg + self.excinfo = excinfo + + def __repr__(self): + if self.msg: + return repr(self.msg) + return "<%s instance>" %(self.__class__.__name__,) + __str__ = __repr__ + +class Skipped(OutcomeException): + # XXX slighly hackish: on 3k we fake to live in the builtins + # in order to have Skipped exception printing shorter/nicer + __module__ = 'builtins' + +class Failed(OutcomeException): + """ raised from an explicit call to py.test.fail() """ + +class ExceptionFailure(Failed): + """ raised by py.test.raises on an exception-assertion mismatch. """ + def __init__(self, expr, expected, msg=None, excinfo=None): + Failed.__init__(self, msg=msg, excinfo=excinfo) + self.expr = expr + self.expected = expected + +class Exit(KeyboardInterrupt): + """ raised by py.test.exit for immediate program exits without tracebacks and reporter/summary. """ + 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) + +def skip(msg=""): + """ skip an executing test with the given message. Note: it's usually + better use the py.test.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) + +def fail(msg=""): + """ explicitely fail an currently-executing test with the given Message. """ + __tracebackhide__ = True + raise Failed(msg=msg) + +def raises(ExpectedException, *args, **kwargs): + """ if args[0] is callable: raise AssertionError if calling it with + the remaining arguments does not raise the expected exception. + if args[0] is a string: raise AssertionError if executing the + the string in the calling scope does not raise expected exception. + for examples: + x = 5 + raises(TypeError, lambda x: x + 'hello', x=x) + raises(TypeError, "x + 'hello'") + """ + __tracebackhide__ = True + assert args + if isinstance(args[0], str): + code, = args + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + #print "raises frame scope: %r" % frame.f_locals + try: + code = py.code.Source(code).compile() + py.builtin.exec_(code, frame.f_globals, loc) + # XXX didn'T mean f_globals == f_locals something special? + # this is destroyed here ... + except ExpectedException: + return py.code.ExceptionInfo() + else: + func = args[0] + try: + func(*args[1:], **kwargs) + except ExpectedException: + return py.code.ExceptionInfo() + k = ", ".join(["%s=%r" % x for x in kwargs.items()]) + if k: + k = ', ' + k + expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) + raise ExceptionFailure(msg="DID NOT RAISE", + expr=args, expected=ExpectedException) + +def importorskip(modname, minversion=None): + """ return imported module if it has a higher __version__ than the + optionally specified 'minversion' - otherwise call py.test.skip() + with a message detailing the mismatch. + """ + compile(modname, '', 'eval') # to catch syntaxerrors + try: + mod = __import__(modname, None, None, ['__doc__']) + except ImportError: + py.test.skip("could not import %r" %(modname,)) + if minversion is None: + return mod + verattr = getattr(mod, '__version__', None) + if isinstance(minversion, str): + minver = minversion.split(".") + else: + minver = list(minversion) + if verattr is None or verattr.split(".") < minver: + py.test.skip("module %r has __version__ %r, required is: %r" %( + modname, verattr, minversion)) + return mod + diff --git a/py/_test/outcome.py b/py/_test/outcome.py deleted file mode 100644 index f58f396c1..000000000 --- a/py/_test/outcome.py +++ /dev/null @@ -1,136 +0,0 @@ -""" - Test OutcomeExceptions and helpers for creating them. - py.test.skip|fail|raises helper implementations - -""" - -import py -import sys - -class OutcomeException(Exception): - """ OutcomeException and its subclass instances indicate and - contain info about test and collection outcomes. - """ - def __init__(self, msg=None, excinfo=None): - self.msg = msg - self.excinfo = excinfo - - def __repr__(self): - if self.msg: - return repr(self.msg) - return "<%s instance>" %(self.__class__.__name__,) - __str__ = __repr__ - -class Passed(OutcomeException): - pass - -class Skipped(OutcomeException): - # XXX slighly hackish: on 3k we fake to live in the builtins - # in order to have Skipped exception printing shorter/nicer - __module__ = 'builtins' - -class Failed(OutcomeException): - pass - -class ExceptionFailure(Failed): - def __init__(self, expr, expected, msg=None, excinfo=None): - Failed.__init__(self, msg=msg, excinfo=excinfo) - self.expr = expr - self.expected = expected - -class Exit(KeyboardInterrupt): - """ for immediate program exits without tracebacks and reporter/summary. """ - 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) - -def skip(msg=""): - """ skip an executing test with the given message. Note: it's usually - better use the py.test.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) - -def fail(msg=""): - """ explicitely fail this executing test with the given Message. """ - __tracebackhide__ = True - raise Failed(msg=msg) - -def raises(ExpectedException, *args, **kwargs): - """ if args[0] is callable: raise AssertionError if calling it with - the remaining arguments does not raise the expected exception. - if args[0] is a string: raise AssertionError if executing the - the string in the calling scope does not raise expected exception. - for examples: - x = 5 - raises(TypeError, lambda x: x + 'hello', x=x) - raises(TypeError, "x + 'hello'") - """ - __tracebackhide__ = True - assert args - if isinstance(args[0], str): - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - #print "raises frame scope: %r" % frame.f_locals - try: - code = py.code.Source(code).compile() - py.builtin.exec_(code, frame.f_globals, loc) - # XXX didn'T mean f_globals == f_locals something special? - # this is destroyed here ... - except ExpectedException: - return py.code.ExceptionInfo() - else: - func = args[0] - try: - func(*args[1:], **kwargs) - except ExpectedException: - return py.code.ExceptionInfo() - k = ", ".join(["%s=%r" % x for x in kwargs.items()]) - if k: - k = ', ' + k - expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) - raise ExceptionFailure(msg="DID NOT RAISE", - expr=args, expected=ExpectedException) - -def importorskip(modname, minversion=None): - """ return imported module if it has a higher __version__ than the - optionally specified 'minversion' - otherwise call py.test.skip() - with a message detailing the mismatch. - """ - compile(modname, '', 'eval') # to catch syntaxerrors - try: - mod = __import__(modname, None, None, ['__doc__']) - except ImportError: - py.test.skip("could not import %r" %(modname,)) - if minversion is None: - return mod - verattr = getattr(mod, '__version__', None) - if isinstance(minversion, str): - minver = minversion.split(".") - else: - minver = list(minversion) - if verattr is None or verattr.split(".") < minver: - py.test.skip("module %r has __version__ %r, required is: %r" %( - modname, verattr, minversion)) - return mod - - - -# exitcodes for the command line -EXIT_OK = 0 -EXIT_TESTSFAILED = 1 -EXIT_INTERRUPTED = 2 -EXIT_INTERNALERROR = 3 -EXIT_NOHOSTS = 4 diff --git a/py/_test/pluginmanager.py b/py/_test/pluginmanager.py index 6b80d4e45..b84a9938b 100644 --- a/py/_test/pluginmanager.py +++ b/py/_test/pluginmanager.py @@ -4,7 +4,6 @@ managing loading and interacting with pytest plugins. import py import inspect from py._plugin import hookspec -from py._test.outcome import Skipped default_plugins = ( "default runner capture mark terminal skipping tmpdir monkeypatch " @@ -139,7 +138,7 @@ class PluginManager(object): mod = importplugin(modname) except KeyboardInterrupt: raise - except Skipped: + except py.test.exc.Skipped: e = py.std.sys.exc_info()[1] self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) else: diff --git a/py/_test/session.py b/py/_test/session.py index 2680aa6fd..7f11e7508 100644 --- a/py/_test/session.py +++ b/py/_test/session.py @@ -6,7 +6,13 @@ """ import py -from py._test import outcome + +# exitcodes for the command line +EXIT_OK = 0 +EXIT_TESTSFAILED = 1 +EXIT_INTERRUPTED = 2 +EXIT_INTERNALERROR = 3 +EXIT_NOHOSTS = 4 # imports used for genitems() Item = py.test.collect.Item @@ -96,21 +102,21 @@ class Session(object): """ main loop for running tests. """ self.shouldstop = False self.sessionstarts() - exitstatus = outcome.EXIT_OK + exitstatus = EXIT_OK try: self._mainloop(colitems) if self._testsfailed: - exitstatus = outcome.EXIT_TESTSFAILED + exitstatus = EXIT_TESTSFAILED self.sessionfinishes(exitstatus=exitstatus) except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - exitstatus = outcome.EXIT_INTERRUPTED + exitstatus = EXIT_INTERRUPTED except: excinfo = py.code.ExceptionInfo() self.config.pluginmanager.notify_exception(excinfo) - exitstatus = outcome.EXIT_INTERNALERROR - if exitstatus in (outcome.EXIT_INTERNALERROR, outcome.EXIT_INTERRUPTED): + exitstatus = EXIT_INTERNALERROR + if exitstatus in (EXIT_INTERNALERROR, EXIT_INTERRUPTED): self.sessionfinishes(exitstatus=exitstatus) return exitstatus diff --git a/testing/plugin/test_pytest_doctest.py b/testing/plugin/test_pytest_doctest.py index b280b6447..52fc25902 100644 --- a/testing/plugin/test_pytest_doctest.py +++ b/testing/plugin/test_pytest_doctest.py @@ -45,8 +45,6 @@ class TestDoctests: reprec.assertoutcome(failed=1) def test_doctest_unexpected_exception(self, testdir): - from py._test.outcome import Failed - p = testdir.maketxtfile(""" >>> i = 0 >>> i = 1 diff --git a/testing/plugin/test_pytest_runner.py b/testing/plugin/test_pytest_runner.py index 963847d8f..462069ef0 100644 --- a/testing/plugin/test_pytest_runner.py +++ b/testing/plugin/test_pytest_runner.py @@ -197,14 +197,13 @@ class BaseFunctionalTests: assert rep.when == "call" def test_exit_propagates(self, testdir): - from py._test.outcome import Exit try: testdir.runitem(""" - from py._test.outcome import Exit + import py def test_func(): - raise Exit() + raise py.test.exc.Exit() """) - except Exit: + except py.test.exc.Exit: pass else: py.test.fail("did not raise") @@ -216,7 +215,6 @@ class TestExecutionNonForked(BaseFunctionalTests): return f def test_keyboardinterrupt_propagates(self, testdir): - from py._test.outcome import Exit try: testdir.runitem(""" def test_func(): diff --git a/testing/root/test_py_imports.py b/testing/root/test_py_imports.py index 901b304bd..01b504431 100644 --- a/testing/root/test_py_imports.py +++ b/testing/root/test_py_imports.py @@ -1,7 +1,6 @@ import py import types import sys -from py._test.outcome import Skipped def checksubpackage(name): obj = getattr(py, name) @@ -52,7 +51,7 @@ def test_importall(): modpath = 'py.%s' % relpath try: check_import(modpath) - except Skipped: + except py.test.exc.Skipped: pass def check_import(modpath): diff --git a/testing/test_config.py b/testing/test_config.py index 6a172fc93..5c548587e 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -75,13 +75,14 @@ class TestConfigAPI: py.test.raises(KeyError, 'config.getvalue("y", o)') def test_config_getvalueorskip(self, testdir): - from py._test.outcome import Skipped config = testdir.parseconfig() - py.test.raises(Skipped, "config.getvalueorskip('hello')") + py.test.raises(py.test.exc.Skipped, + "config.getvalueorskip('hello')") verbose = config.getvalueorskip("verbose") assert verbose == config.option.verbose config.option.hello = None - py.test.raises(Skipped, "config.getvalueorskip('hello')") + py.test.raises(py.test.exc.Skipped, + "config.getvalueorskip('hello')") def test_config_overwrite(self, testdir): o = testdir.tmpdir diff --git a/testing/test_deprecated_api.py b/testing/test_deprecated_api.py index 744c47650..8d624c880 100644 --- a/testing/test_deprecated_api.py +++ b/testing/test_deprecated_api.py @@ -1,6 +1,5 @@ import py -from py._test.outcome import Skipped class TestCollectDeprecated: @@ -191,7 +190,7 @@ class TestDisabled: l = modcol.collect() assert len(l) == 1 recwarn.clear() - py.test.raises(Skipped, "modcol.setup()") + py.test.raises(py.test.exc.Skipped, "modcol.setup()") recwarn.pop(DeprecationWarning) def test_disabled_class(self, recwarn, testdir): @@ -208,7 +207,7 @@ class TestDisabled: l = modcol.collect() assert len(l) == 1 recwarn.clear() - py.test.raises(Skipped, "modcol.setup()") + py.test.raises(py.test.exc.Skipped, "modcol.setup()") recwarn.pop(DeprecationWarning) def test_disabled_class_functional(self, testdir): diff --git a/testing/test_outcome.py b/testing/test_outcome.py index a7a782aba..6cdc6409c 100644 --- a/testing/test_outcome.py +++ b/testing/test_outcome.py @@ -16,13 +16,12 @@ class TestRaises: py.test.raises(ValueError, int, 'hello') def test_raises_callable_no_exception(self): - from py._test.outcome import ExceptionFailure class A: def __call__(self): pass try: py.test.raises(ValueError, A()) - except ExceptionFailure: + except py.test.exc.ExceptionFailure: pass def test_pytest_exit(): @@ -41,23 +40,23 @@ def test_exception_printing_skip(): assert s.startswith("Skipped") def test_importorskip(): - from py._test.outcome import Skipped, importorskip - assert importorskip == py.test.importorskip + importorskip = py.test.importorskip try: sys = importorskip("sys") assert sys == py.std.sys #path = py.test.importorskip("os.path") #assert path == py.std.os.path - py.test.raises(Skipped, "py.test.importorskip('alskdj')") + py.test.raises(py.test.exc.Skipped, + "py.test.importorskip('alskdj')") py.test.raises(SyntaxError, "py.test.importorskip('x y z')") py.test.raises(SyntaxError, "py.test.importorskip('x=y')") path = importorskip("py", minversion=".".join(py.__version__)) mod = py.std.types.ModuleType("hello123") mod.__version__ = "1.3" - py.test.raises(Skipped, """ + py.test.raises(py.test.exc.Skipped, """ py.test.importorskip("hello123", minversion="5.0") """) - except Skipped: + except py.test.exc.Skipped: print(py.code.ExceptionInfo()) py.test.fail("spurious skip") diff --git a/testing/test_pycollect.py b/testing/test_pycollect.py index 5a3b268aa..331d9e4df 100644 --- a/testing/test_pycollect.py +++ b/testing/test_pycollect.py @@ -444,8 +444,7 @@ def test_modulecol_roundtrip(testdir): class TestTracebackCutting: def test_skip_simple(self): - from py._test.outcome import Skipped - excinfo = py.test.raises(Skipped, 'py.test.skip("xxx")') + excinfo = py.test.raises(py.test.exc.Skipped, 'py.test.skip("xxx")') assert excinfo.traceback[-1].frame.code.name == "skip" assert excinfo.traceback[-1].ishidden()