Commit 664c6000 authored by Charles Ferguson's avatar Charles Ferguson
Browse files

Initial import of the Timer module which generates timeouts.

The Timer module provides the ability to interrupt after a timeout,
and time events. Multiple timers are supported.
parents
# All text files should be LFs
* text=auto eol=lf
*.pyc
build-docs
/.project
venv
venv3
.coverage
htmlcov
nosetests.xml
lint:
stage: build
script:
- bash scripts/lint
python2:
stage: test
script:
- bash scripts/test
python3:
stage: test
script:
- bash scripts/test -3
docs:
stage: build
script:
- bash scripts/docs
artifacts:
paths:
- build-docs
stages:
- build
- test
#!/usr/bin/env python
"""
Timer management for setting arbitrary timeouts on OSX and Linux.
"""
import os
import new
import signal
import time
# The smallest amount of time we may use for alarms, which must be greater than 0.
MINIMUM_ALARM_INTERVAL = 0.001
class AlarmError(Exception):
pass
class AlarmInstance(object):
"""
A record of the point at which an alarm should be triggered.
"""
def __init__(self, seconds, handler):
if seconds <= 0:
raise ValueError('Alarm timeout must be positive and non-0')
self.seconds = seconds
self.end_time = time.time() + seconds
self.handler = handler
def reset(self, seconds):
if seconds is not None:
self.seconds = seconds
self.end_time = time.time() + seconds
return self
def __repr__(self):
left = self.end_time - time.time()
return "AlarmInstance(in %.2f seconds)" % (left,)
def __cmp__(self, other):
if not isinstance(other, AlarmInstance):
raise TypeError('AlarmInstances can only be compared with other AlarmInstances')
return self.end_time - other.end_time
class AlarmManager(object):
"""
Manager for the single timeout available to the system.
There is only one ALRM timeout available to us. As such, we need to be able to
provide a mechanism to set multiple nested alarms, which will expire at the
appropriate time.
There is only ever one AlarmManager object which arbitrates the alarm calls.
"""
def __init__(self):
self.active_alarms = []
self.current_alarm = None
self.active = False
def _reset_alarm(self):
if not self.active_alarms:
# There are no active alarms ...
if self.active:
# ... but we are active, so restore default handler.
signal.signal(signal.SIGALRM, signal.SIG_DFL)
signal.alarm(0)
self.active = False
else:
# ... and we're not active, so nothing to do.
pass
return
# There are alarms present.
if not self.active:
# We are not active, so we need to take over the current handler so that we can manage it.
old_handler = signal.getsignal(signal.SIGALRM)
if old_handler != signal.SIG_IGN:
# We need to fake a new handler for the existing alarm.
(remaining_time, repeating_time) = signal.getitimer(signal.ITIMER_REAL)
if remaining_time > 0:
def call_old_handler(alarm, frame, old_handler=old_handler, repeating_time=repeating_time):
if old_handler == signal.SIG_DFL:
# Raise the signal through the default handler (which appears to be untrappable)
signal.signal(signal.SIGALRM, signal.SIG_DFL)
os.kill(0, signal.SIGALRM)
old_handler(signal.SIGALRM, frame)
if repeating_time:
Alarm.reset(repeating_time)
self.current_alarm = AlarmInstance(remaining_time, call_old_handler)
self.active_alarms.append(self.current_alarm)
self.active = True
signal.signal(signal.SIGALRM, self._alarm_handler)
# This is not the most efficient solution; if we've just added an alarm, it might be
# cheaper to just check against that one alarm. However, this is a lot simpler.
next_alarm = min(self.active_alarms)
if next_alarm is not self.current_alarm:
# We need to change the time that the alarm happens
self.current_alarm = next_alarm
next_time = next_alarm.end_time - time.time()
next_time = max(MINIMUM_ALARM_INTERVAL, next_time)
print("Next alarm will be in %s seconds: %s" % (next_time, next_alarm))
signal.setitimer(signal.ITIMER_REAL, next_time)
def _alarm_handler(self, sig, frame):
"""
Raw signal handler, so that we can dispatch to the registered handlers.
"""
# Alarms will be suspended for the duration of the handler, as this makes it significantly
# easier to handle alarm cases reliably, without having problems with alarms going off whilst
# an alarm is being handled. Otherwise the alarm handler might have to handle itself being
# interrupted and not cleaned up.
signal.setitimer(0, 0)
# The currently active alarm handler is the one that we'll now call.
alarm = self.current_alarm
print("Removed triggered alarm (%s)" % (alarm,))
self.active_alarms.remove(alarm)
self.current_alarm = None
try:
if alarm.handler is None:
raise AlarmError('Timeout after %s seconds' % (alarm.seconds,))
alarm.handler(alarm, frame)
finally:
self._reset_alarm()
def reset(self, alarm, timeout=None):
alarm.reset(timeout)
self._reset_alarm()
def set(self, timeout, handler):
timeout = max(MINIMUM_ALARM_INTERVAL, timeout)
alarm = AlarmInstance(timeout, handler)
print("Set alarm for %s seconds (%s)" % (timeout, alarm))
self.active_alarms.append(alarm)
self._reset_alarm()
return alarm
def clear(self, alarm):
if alarm in self.active_alarms:
print("Removed alarm (%s)" % (alarm,))
self.active_alarms.remove(alarm)
self._reset_alarm()
# The single instance of the alarm manager
Alarm = AlarmManager()
class Timer(object):
"""
Timer, optionally with timeout handler.
If no handler is defined, we will raise an exception for this instance of the Alarm.
If no timeout is defined, we
The objects can be used directly, or as a context handler.
Context handler (using default of raising an exception on timeout)::
with Timer(60) as timer:
long_operation()
Direct usage (using default of raising an exception on timeout)::
timer = Timer(60)
timer.start()
long_operation()
timer.stop()
Trapping the specific exception::
timer = Timer(60)
try:
with timer:
long_operation()
except timer.exception as exc:
print "Trapped the timeout exception"
Providing a handler to do something else, such as terminating at a convenient point::
stop = False
def handler(frame):
global stop
stop = True
with Timer(60, handler=myfunc) as timer:
while not stop:
short_operation()
Giving the alarms names may help to debug them::
with Timer(60, name='1Minute') as timer:
long_operation()
Timing events is easy with the same interface, too::
with Timer() as timer:
do_something()
print "Took %s seconds" % (timer.elapsed,)
"""
def __init__(self, timeout_in_seconds=None, handler=None, name=None):
self.timeout_in_seconds = timeout_in_seconds
self.handler = handler or self.default_handler
self.idname = name or id(self)
self.name = name or '<unnamed>'
self.elapsed = 0
self.running = False
self.start_time = None
self.end_time = None
if self.timeout_in_seconds:
self.exception = new.classobj('TimerError_%s' % (self.idname,), (AlarmError,), {})
else:
self.exception = None
self.alarm = None
def __repr__(self):
elapsed = self.elapsed
if self.running:
elapsed += time.time() - self.start_time
state = 'running' if self.running else 'waiting'
return "Timer(%s, %s, %.2f of %.2f elapsed)" % (self.name, state, elapsed, self.timeout_in_seconds)
def __del__(self):
if getattr(self, 'alarm', None):
Alarm.clear(self.alarm)
self.alarm = None
def _alarm_handler(self, alarm, frame):
self.stop()
self.handler(frame)
def default_handler(self, frame):
raise self.exception('Timeout of %s seconds reached' % (self.timeout_in_seconds,))
def reset(self, timeout):
self.stop()
self.timeout_in_seconds = timeout
self.elapsed = 0
self.start()
def delay(self, extra_timeout):
self.stop()
self.timeout_in_seconds += extra_timeout
self.start()
def start(self):
if self.running:
raise TypeError('Timer object is already running (cannot be started)')
print("Start Timer %s" % (self.name,))
self.start_time = time.time()
if self.timeout_in_seconds is not None:
# Work out when we should stop
self.end_time = self.start_time + self.timeout_in_seconds - self.elapsed
self.alarm = Alarm.set(self.end_time - self.start_time, self._alarm_handler)
self.running = True
def stop(self):
if not self.running:
# Do not raise an exception if stopped multiple times - allows error handling
# of the object to be more easily managed.
return
print("Stop Timer %s" % (self.name,))
if self.alarm:
Alarm.clear(self.alarm)
self.alarm = None
self.end_time = time.time()
self.elapsed += self.end_time - self.start_time
self.running = False
def __enter__(self):
self.start()
return self
def __exit__(self, exc_class, exc, msg):
self.stop()
with Timer(2, name='Outer') as t1:
try:
for i in range(0, 12):
print("Iteration %s (t1=%s)" % (i, t1))
t2 = Timer(5, name='Inner')
try:
with t2:
time.sleep(10)
except t2.exception as ex:
print("Got a 't2' exception: %s" % (ex,))
except t1.exception as ex:
print("!!! Got a 't1' exception: %s" % (ex,))
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=no
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable all messages by default.
enable=all
# Disable all messages we disagree with.
# N.B. Although the documentation suggests we can safely use multiple lines for 'enable' and 'disable' keywords, this is
# not true: they must all be listed on one long line.
disable=missing-docstring, too-few-public-methods, too-many-locals, too-many-branches, too-many-public-methods, fixme, too-many-statements, too-many-return-statements, too-many-instance-attributes, too-many-arguments, too-many-lines, invalid-name, locally-disabled, suppressed-message, superfluous-parens
# TODO We need to re-enable the following checks at some point:
# - bad-continuation
# - too-many-return-statements
# - too-many-instance-attributes
# - too-many-arguments
# - missing-docstring
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
msg-template={msg_id}:{line:3d},{column}: {obj}: {msg} [{symbol}]
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map, filter, input, assert
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_,e,log,m,maxDiff,fh,venv
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=yes
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,40}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,40}|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,40}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=__.*__
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.