WrapItUp v0.3.0¶
Facilitate interrupting slow code with signals and time limits.
Scripts can request that long running processes gracefully exit, or shut
down by calling request(). Those long running processes can
listen, thereby becoming listeners, by querying requested()
occasionally. Scripts can also pass listeners time limits, which the listeners
can track with Timer instances; since those instances also check for
requests to shut down, listeners can encapsulate all their listening directly
via Timer’s Timer.remaining() and Timer.expired() methods.
Scripts can allow users to interrupt listeners using signals or Ctrl+C
via catch_signals(). It returns a context manager inside of which the
receipt of specified signals triggers request().
Example¶
A typical use of this package might look like the following.
my_script.py¶1 2 3 4 | import wrapitup
from my_library import a_lot_of_work
with wrapitup.catch_signals():
a_lot_of_work(data, time_limit)
|
my_library.py¶1 2 3 4 5 6 7 | import wrapitup
def a_lot_of_work(data, time_limit):
timer = wrapitup.Timer(time_limit)
for datum in data:
if timer.expired():
break
do_work(datum)
|
Then timer.expired() in my_library.py will be True, and
break out of the for loop, upon the earlier of
- the time limit having run out or
- the process receiving a Ctrl+C or similar signal.
Note
The request API — request(), requested(), and reset()
— is thread safe, but catch_signals() must be called from the main
thread only. Timer instances require external synchronization if you
want to rely on their timing features.
Request API¶
Signals¶
-
wrapitup.catch_signals(signals: Iterable[Union[int, str]] = (<Signals.SIGINT: 2>, <Signals.SIGTERM: 15>), callback: Optional[Callable[[signal.Signals, Optional[frame]], None]] = None)[source]¶ Return a context manager to catch signals to request listeners to shut down.
Upon entrance to the context manager, a message at the
logging.INFOlevel is written to the logger whose name is this module’s__package__advising which signals are being listend for. Entrance to the context manager returns nothing and thus binds nothing butNoneto the target ofas.Upon receipt of one of the signals in
signals,catch_signals()callsrequest(), replaces the remaining signal handlers with those installed beforecatch_signals(), and finally callscallback.When the context manager exits the
withblock, or when any of the installed handlers catches its corresponding signal, all the signal handlers installed before the block started will be reinstalled unconditionally. When thewithblock exits, the value returned byrequested()will be returned unconditionally to its value before entrance to thewithblock.catch_signals()instances are reentrant and reusable. However, keep in mind that signals and the state ofrequested()are global.Availability: Unix (including macOS and Linux), Windows.
Note
catch_signals()must be used from the main thread only, or it will raise aValueError. Note that if you’re running listeners in multiple threads started in thewithblock, you must join them in the same block or there will be a race between uninstalling the signal handlers and finishing the listeners.Note
On Windows, only processes attached to a console session can receive Ctrl+C or Ctrl+Break events, which are the only signals Python really supports on Windows. To check whether the current process is attached to a console, import
sys.sys.__stderr__.isatty()returns whether the process is attached to a console. The only console that seems to work in the tests ofcatch_signals()is cmd.exe.Parameters: - signals –
Signals from the
signalmodule to listen for. If the objects in the iterable areints orstrs,catch_signals()attempts to convert them tosignal.Signals. The default includesSIGINT, which Ctrl+C sends and normally causes Python to raise aKeyboardInterrupt.- On Windows
signalsmust contain no signals other thanSIGINTorSIGBREAK, the two of which are together the default.- On Unix
- the default is
SIGINTandSIGTERM. On macOS,SIGTERMis what Activity Monitor sends to processes when you select a program and hit .
- callback – Called from within the installed signal handlers with the
arguments that the Python
signal()system passes to the handler, except that thesignumargument is converted to typesignal.Signalsfirst. The default, used if the argument isNone, logs the event at thelogging.WARNINGlevel to the logger whose name is this module’s__package__.
Raises: - KeyError – If the
signalmodule does not recognize a string signal name insignals. - TypeError – If
callbackisn’t a callable taking two positional arguments. - ValueError – If called from a thread other than the main thread, or
if
signalsis empty, or, ifsignalscontains objects that cannot be converted toSignalstype, or, on Windows, ifsignalscontains signals other than those allowed.
Returns: A context manager to use in a
withblock.New in version 0.2.0: The callback parameter.
New in version 0.3.0: Windows support.
Changed in version 0.3.0:
catch_signals()became reentrant and reusable.- signals –
Timer¶
-
class
wrapitup.Timer(limit: float = inf)[source]¶ Bases:
objectCountdown timer that goes to zero while a request to shut down is active.
The timer starts with a time limit in seconds. Pass a time limit to the class’s constructor or
start(); in both places, the time limit defaults to infinity (float('inf')).Methods
remaining()andexpired()act as though the timer ran into its time limit if a shut down has been requested viarequest()(whichcatch_signals()uses). However, the timer can continue as if nothing happened ifreset()is called.Parameters: limit (float) – Time limit after which this timer expires, in seconds.
Raises: - TypeError – if
limitis not afloatorint. - ValueError – if
limitis not a number (NaN).
Changed in version 0.2.0: Renamed from
Shutter. Constructor argument name changed fromtimeout.-
alarm() → Tuple[float, float][source]¶ Send the
signal.SIGALRMsignal when the time limit expires.Despite the name, this method uses
signal.setitimer(), notsignal.alarm(). The previoussignal.ITIMER_REALtimer’s seconds and interval arguments are returned in case you want to restore it later. This method does not set an interval, so the signal is delivered only once. Don’t forget to set a handler forsignal.SIGALRMbefore the signal arrives.Availability: Unix.
Returns: seconds: The previous signal.ITIMER_REALtimer’ssecondsargument forsignal.setitimer(). Zero if no previous timer existed.interval: The previous signal.ITIMER_REALtimer’sintervalargument forsignal.setitimer().Raises: ValueError – If the time limit expired, so that remaining()returns negative, before setting the alarm.New in version 0.2.0.
-
expired() → bool[source]¶ Return whether the time limit has expired or a shut down was requested.
Returns: Changed in version 0.2.0: Renamed from
timedout.
-
remaining() → float[source]¶ Return amount of time remaining under the time limit.
Returns: Time in seconds remaining under the time limit. If a shut down was requested through request(), return zero. This can change ifreset()is called later.Changed in version 0.2.0: Renamed from
time_left.
-
start(limit: float = inf) → None[source]¶ (Re)start the timer. If restarting, replaces the time limit.
Parameters: limit (float) – Time limit after which this timer expires, in seconds.
Raises: - TypeError – if
limitis not afloatorint. - ValueError – if
limitis not a number (NaN).
Changed in version 0.2.0: Renamed from
start_timer, and argument name changed fromtimeout.- TypeError – if
- TypeError – if