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

  1. the time limit having run out or
  2. 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

wrapitup.request() → None[source]

Request all listeners running in this process to shut down.

wrapitup.requested() → bool[source]

Return whether listeners should shut down.

wrapitup.reset() → None[source]

Stop requesting listeners running in this process to shut down.

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.INFO level 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 but None to the target of as.

Upon receipt of one of the signals in signals, catch_signals() calls request(), replaces the remaining signal handlers with those installed before catch_signals(), and finally calls callback.

When the context manager exits the with block, 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 the with block exits, the value returned by requested() will be returned unconditionally to its value before entrance to the with block.

catch_signals() instances are reentrant and reusable. However, keep in mind that signals and the state of requested() are global.

Availability: Unix (including macOS and Linux), Windows.

Note

catch_signals() must be used from the main thread only, or it will raise a ValueError. Note that if you’re running listeners in multiple threads started in the with block, 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 of catch_signals() is cmd.exe.

Parameters:
  • signals –

    Signals from the signal module to listen for. If the objects in the iterable are ints or strs, catch_signals() attempts to convert them to signal.Signals. The default includes SIGINT, which Ctrl+C sends and normally causes Python to raise a KeyboardInterrupt.

    On Windows
    signals must contain no signals other than SIGINT or SIGBREAK, the two of which are together the default.
    On Unix
    the default is SIGINT and SIGTERM. On macOS, SIGTERM is what Activity Monitor sends to processes when you select a program and hit 🛑 ‣ Quit.
  • callback – Called from within the installed signal handlers with the arguments that the Python signal() system passes to the handler, except that the signum argument is converted to type signal.Signals first. The default, used if the argument is None, logs the event at the logging.WARNING level to the logger whose name is this module’s __package__.
Raises:
  • KeyError – If the signal module does not recognize a string signal name in signals.
  • TypeError – If callback isn’t a callable taking two positional arguments.
  • ValueError – If called from a thread other than the main thread, or if signals is empty, or, if signals contains objects that cannot be converted to Signals type, or, on Windows, if signals contains signals other than those allowed.
Returns:

A context manager to use in a with block.

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.

Timer

class wrapitup.Timer(limit: float = inf)[source]

Bases: object

Countdown 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() and expired() act as though the timer ran into its time limit if a shut down has been requested via request() (which catch_signals() uses). However, the timer can continue as if nothing happened if reset() is called.

Parameters:

limit (float) – Time limit after which this timer expires, in seconds.

Raises:

Changed in version 0.2.0: Renamed from Shutter. Constructor argument name changed from timeout.

alarm() → Tuple[float, float][source]

Send the signal.SIGALRM signal when the time limit expires.

Despite the name, this method uses signal.setitimer(), not signal.alarm(). The previous signal.ITIMER_REAL timer’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 for signal.SIGALRM before the signal arrives.

Availability: Unix.

Returns:
seconds:The previous signal.ITIMER_REAL timer’s seconds argument for signal.setitimer(). Zero if no previous timer existed.
interval:The previous signal.ITIMER_REAL timer’s interval argument for signal.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:
  • Before stop() is called, expired() returns whether
    time remains within the time limit or if a shut down was requested through request(). This can change if reset() is called later.
  • After stop() is called, expired() returns whether a
    shut down was requested at the time stop() was called or if the total running time exceeded the time limit. This cannot change if reset() is called later.

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 if reset() 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:

Changed in version 0.2.0: Renamed from start_timer, and argument name changed from timeout.

stop() → float[source]

Stop and return elapsed time.

Returns:Time in seconds between more recent of construction or call to start() and the first call to stop(). Subsequent calls after the first return the same value as the first one does.

Changed in version 0.2.0: Renamed from stop_timer.

Indices and tables