From 5c322ece9601da2a4113f489d44762abffe6b8cd Mon Sep 17 00:00:00 2001 From: Michael Foord Date: Sun, 25 Apr 2010 19:02:46 +0000 Subject: [PATCH] Adding unittest.removeHandler function / decorator for removing the signal.SIGINT signal handler. With tests and docs. --- Doc/library/unittest.rst | 27 +++++++++++++++++++++++++-- Lib/unittest/__init__.py | 4 ++-- Lib/unittest/signals.py | 19 +++++++++++++++++++ Lib/unittest/test/test_break.py | 21 +++++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index f28f4f0dbf8..66ac57c5ed8 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1855,8 +1855,17 @@ allow the currently running test to complete, and the test run will then end and report all the results so far. A second control-c will raise a ``KeyboardInterrupt`` in the usual way. -There are a few utility functions for framework authors to enable this -functionality within test frameworks. +The control-c handling signal handler attempts to remain compatible with code or +tests that install their own :const:`signal.SIGINT` handler. If the ``unittest`` +handler is called but *isn't* the installed :const:`signal.SIGINT` handler, +i.e. it has been replaced by the system under test and delegated to, then it +calls the default handler. This will normally be the expected behavior by code +that replaces an installed handler and delegates to it. For individual tests +that need ``unittest`` control-c handling disabled the :func:`removeHandler` +decorator can be used. + +There are a few utility functions for framework authors to enable control-c +handling functionality within test frameworks. .. function:: installHandler() @@ -1870,9 +1879,23 @@ functionality within test frameworks. result stores a weak reference to it, so it doesn't prevent the result from being garbage collected. + Registering a :class:`TestResult` object has no side-effects if control-c + handling is not enabled, so test frameworks can unconditionally register + all results they create independently of whether or not handling is enabled. + .. function:: removeResult(result) Remove a registered result. Once a result has been removed then :meth:`~TestResult.stop` will no longer be called on that result object in response to a control-c. +.. function:: removeHandler(function=None) + + When called without arguments this function removes the control-c handler + if it has been installed. This function can also be used as a test decorator + to temporarily remove the handler whilst the test is being executed:: + + @unittest.removeHandler + def test_signal_handling(self): + ... + diff --git a/Lib/unittest/__init__.py b/Lib/unittest/__init__.py index e84299e993a..201a3f0d2da 100644 --- a/Lib/unittest/__init__.py +++ b/Lib/unittest/__init__.py @@ -48,7 +48,7 @@ __all__ = ['TestResult', 'TestCase', 'TestSuite', 'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main', 'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless', 'expectedFailure', 'TextTestResult', 'installHandler', - 'registerResult', 'removeResult'] + 'registerResult', 'removeResult', 'removeHandler'] # Expose obsolete functions for backwards compatibility __all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases']) @@ -63,7 +63,7 @@ from .loader import (TestLoader, defaultTestLoader, makeSuite, getTestCaseNames, findTestCases) from .main import TestProgram, main from .runner import TextTestRunner, TextTestResult -from .signals import installHandler, registerResult, removeResult +from .signals import installHandler, registerResult, removeResult, removeHandler # deprecated _TextTestResult = TextTestResult diff --git a/Lib/unittest/signals.py b/Lib/unittest/signals.py index 0651cf2edea..fc31043283b 100644 --- a/Lib/unittest/signals.py +++ b/Lib/unittest/signals.py @@ -1,6 +1,8 @@ import signal import weakref +from functools import wraps + __unittest = True @@ -36,3 +38,20 @@ def installHandler(): default_handler = signal.getsignal(signal.SIGINT) _interrupt_handler = _InterruptHandler(default_handler) signal.signal(signal.SIGINT, _interrupt_handler) + + +def removeHandler(method=None): + if method is not None: + @wraps(method) + def inner(*args, **kwargs): + initial = signal.getsignal(signal.SIGINT) + removeHandler() + try: + return method(*args, **kwargs) + finally: + signal.signal(signal.SIGINT, initial) + return inner + + global _interrupt_handler + if _interrupt_handler is not None: + signal.signal(signal.SIGINT, _interrupt_handler.default_handler) diff --git a/Lib/unittest/test/test_break.py b/Lib/unittest/test/test_break.py index cc3e2827f47..560001103f8 100644 --- a/Lib/unittest/test/test_break.py +++ b/Lib/unittest/test/test_break.py @@ -229,3 +229,24 @@ class TestBreak(unittest.TestCase): self.assertEqual(p.result, result) self.assertNotEqual(signal.getsignal(signal.SIGINT), default_handler) + + def testRemoveHandler(self): + default_handler = signal.getsignal(signal.SIGINT) + unittest.installHandler() + unittest.removeHandler() + self.assertEqual(signal.getsignal(signal.SIGINT), default_handler) + + # check that calling removeHandler multiple times has no ill-effect + unittest.removeHandler() + self.assertEqual(signal.getsignal(signal.SIGINT), default_handler) + + def testRemoveHandlerAsDecorator(self): + default_handler = signal.getsignal(signal.SIGINT) + unittest.installHandler() + + @unittest.removeHandler + def test(): + self.assertEqual(signal.getsignal(signal.SIGINT), default_handler) + + test() + self.assertNotEqual(signal.getsignal(signal.SIGINT), default_handler)