package structure prepared
--HG-- branch : aspn
This commit is contained in:
360
src/ThreadPool.py
Normal file
360
src/ThreadPool.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
'''
|
||||||
|
Yet another thread pool module.
|
||||||
|
|
||||||
|
A thread pool consists of a set of worker threads for performing time consuming
|
||||||
|
operations concurrently. A minimal API provides a way to submit jobs (requests),
|
||||||
|
without waiting for them to finish, and get the results back in some way once
|
||||||
|
they are available. The thread pool is responsible for assigning jobs to the
|
||||||
|
worker threads by putting them in a job queue, where they are picked up by the
|
||||||
|
next available worker. The worker then performs the assigned job in the background
|
||||||
|
and puts the processed request in an output queue.
|
||||||
|
|
||||||
|
The main novelty of this module compared to other threadpool recipes is the way
|
||||||
|
results are returned to the client. Instead of providing a callback to post-process
|
||||||
|
the computed results, a L{generator <ThreadPool.iterProcessedJobs>} is used for
|
||||||
|
popping the processed jobs from the output queue and yielding them back to the
|
||||||
|
caller. The processed jobs encapsulate the computed result (or raised exception)
|
||||||
|
and can be used transparently by the calling thread, as if the computation didn't
|
||||||
|
take place in a different thread. This is more flexible that the callback-based
|
||||||
|
approach since it gives full control to the caller of when to ask for a result,
|
||||||
|
how long to wait for it and what to do with it once it is fetched.
|
||||||
|
|
||||||
|
After a C{JobRequest} is L{added <ThreadPool.addJob>} to a L{ThreadPool}, it can
|
||||||
|
be in one of the following states:
|
||||||
|
1. Unassigned: The request is still in the input queue, no worker thread
|
||||||
|
has been assigned to it yet. There are two substates:
|
||||||
|
- Pending: The job is waiting its turn to be picked up by a L{Worker}.
|
||||||
|
- Cancelled: The job has been L{cancelled <ThreadPool.cancelJob>} and,
|
||||||
|
although it still occupies a slot in the input queue, it will be
|
||||||
|
discarded when a L{Worker} picks it up.
|
||||||
|
2. In progress: The job has been popped by the input queue by a L{Worker} and
|
||||||
|
is in the process of being executed.
|
||||||
|
3. Processed: The job has been processed (successfully or not) and has been
|
||||||
|
added to the output queue, ready to be returned.
|
||||||
|
4. Returned: The job has been returned to the client, either by
|
||||||
|
L{ThreadPool.iterProcessedJobs} or L{ThreadPool.processedJobs} and is no
|
||||||
|
longer associated with the threadpool.
|
||||||
|
A job in state 1.a, 2 or 3 is said to be I{active}.
|
||||||
|
|
||||||
|
B{Acknowledgements:} The basic concept and the initial implementation was taken
|
||||||
|
from the U{threadpool module of Christopher Arndt
|
||||||
|
<http://www.chrisarndt.de/en/software/python/threadpool/>}, who in turn borrowed
|
||||||
|
from the "Python in a Nutshell" book by Alex Martelli.
|
||||||
|
'''
|
||||||
|
|
||||||
|
__all__ = ['ThreadPool', 'JobRequest']
|
||||||
|
__author__ = 'George Sakkis'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import Queue
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
_log = logging.getLogger('threadpool')
|
||||||
|
|
||||||
|
|
||||||
|
def synchronized(f):
|
||||||
|
'''A synchronized method decorator'''
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try: lock = self.__lock
|
||||||
|
except AttributeError: # first time use
|
||||||
|
lock = self.__dict__.setdefault('__lock', threading.RLock())
|
||||||
|
lock.acquire()
|
||||||
|
try: return f(self, *args, **kwargs)
|
||||||
|
finally: lock.release()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadPool(object):
|
||||||
|
'''A thread pool, distributing job requests and collecting them after they
|
||||||
|
are processed.
|
||||||
|
|
||||||
|
See the module doctring for more information.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, num_workers, input_queue_size=0, output_queue_size=0):
|
||||||
|
'''Set up the thread pool and start C{num_workers} worker threads.
|
||||||
|
|
||||||
|
@param num_workers: The number of worker threads to start initially.
|
||||||
|
@param input_queue_size: If a positive integer, it's the maximum number
|
||||||
|
of unassigned jobs. The thread pool blocks when the queue is full a
|
||||||
|
new job is submitted.
|
||||||
|
@param output_queue_size: If a positive integer, it's the maximum number
|
||||||
|
of completed jobs waiting to be fetched. The thread pool blocks when
|
||||||
|
the queue is full and a job is completed.
|
||||||
|
'''
|
||||||
|
self._workers = []
|
||||||
|
self._activeKey2Job = {}
|
||||||
|
self._unassignedKey2Job = {}
|
||||||
|
self._unassignedJobs = Queue.Queue(input_queue_size)
|
||||||
|
self._processedJobs = Queue.Queue(output_queue_size)
|
||||||
|
self.addWorkers(num_workers)
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def addWorkers(self, n=1):
|
||||||
|
'''Add C{n} worker threads to the pool.'''
|
||||||
|
for _ in xrange(n):
|
||||||
|
self._workers.append(Worker(self._unassignedJobs, self._processedJobs,
|
||||||
|
self._unassignedKey2Job))
|
||||||
|
_log.debug('Added %d workers' % n)
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def dismissWorkers(self, n=1):
|
||||||
|
'Tell C{n} worker threads to quit after they finish with their current job.'
|
||||||
|
for _ in xrange(n):
|
||||||
|
try: self._workers.pop().dismissed = True
|
||||||
|
except KeyError: break
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def addJob(self, job, timeout=None):
|
||||||
|
'''Add a job request to the end of the input queue.
|
||||||
|
|
||||||
|
@param timeout: If the input queue is full and C{timeout is None}, block
|
||||||
|
until a slot becomes available. If C{timeout > 0}, block for up to
|
||||||
|
C{timeout} seconds and raise C{Queue.Full} exception if the queue is
|
||||||
|
still full. If C{timeout <= 0}, do not block and raise C{Queue.Full}
|
||||||
|
immediately if the queue is full.
|
||||||
|
'''
|
||||||
|
key = job.key
|
||||||
|
self._unassignedJobs.put(job, timeout is None or timeout>0, timeout)
|
||||||
|
self._unassignedKey2Job[key] = self._activeKey2Job[key] = job
|
||||||
|
_log.debug('Added job %r to the input queue' % key)
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def cancelJob(self, key):
|
||||||
|
'''Cancel a job.
|
||||||
|
|
||||||
|
This has effect only if the job is still unassigned; if it's in progress
|
||||||
|
or has already been processed, it has no effect.
|
||||||
|
|
||||||
|
@param key: The job's identifier.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
del self._unassignedKey2Job[key]
|
||||||
|
# if it's not in unassigned, it may be in progress or already
|
||||||
|
# processed; don't try to delete it from active
|
||||||
|
del self._activeKey2Job[key]
|
||||||
|
except KeyError: pass
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def cancelAllJobs(self):
|
||||||
|
'''Cancel all unassigned jobs.'''
|
||||||
|
while self._unassignedKey2Job:
|
||||||
|
del self._activeKey2Job[self._unassignedKey2Job.popitem()[0]]
|
||||||
|
|
||||||
|
def numActiveJobs(self):
|
||||||
|
'''Return the approximate number of active jobs.
|
||||||
|
|
||||||
|
This is not reliable due to thread semantics.
|
||||||
|
'''
|
||||||
|
return len(self._activeKey2Job)
|
||||||
|
|
||||||
|
def iterProcessedJobs(self, timeout=None):
|
||||||
|
'''Return an iterator over processed job requests, popping them off the
|
||||||
|
output queue.
|
||||||
|
|
||||||
|
@param timeout: There are three cases:
|
||||||
|
- If C{None}, iterate over the processed jobs as long as there are
|
||||||
|
any active jobs. Whenever there are no processed jobs available,
|
||||||
|
block and wait for a job to finish.
|
||||||
|
- If C{<= 0}, iterate over the currently processed jobs only; do not
|
||||||
|
block.
|
||||||
|
- If C{> 0}, wait up to C{timeout} seconds per processed job as long
|
||||||
|
as there are active jobs. Note that a loop such as::
|
||||||
|
for r in t.iterProcessedJobs(2): pass
|
||||||
|
may take from microseconds (if there are no active jobs) to
|
||||||
|
arbitrarily long time, as long as each processed job is yielded
|
||||||
|
within 2 seconds. If you want a timeout for the whole loop, use
|
||||||
|
L{processedJobs} instead.
|
||||||
|
'''
|
||||||
|
block = timeout is None or timeout>0
|
||||||
|
while self._activeKey2Job:
|
||||||
|
try: job = self._processedJobs.get(block, timeout)
|
||||||
|
except Queue.Empty:
|
||||||
|
break
|
||||||
|
key = job.key
|
||||||
|
_log.debug('Popped job %r from the output queue' % key)
|
||||||
|
# at this point the key is guaranteed to be in _activeKey2Job even
|
||||||
|
# if the job has been cancelled
|
||||||
|
assert key in self._activeKey2Job
|
||||||
|
del self._activeKey2Job[key]
|
||||||
|
yield job
|
||||||
|
|
||||||
|
def processedJobs(self, timeout=None):
|
||||||
|
'''Return a list of processed job requests.
|
||||||
|
|
||||||
|
@param timeout: If C{timeout is None} or C{timeout <= 0}, it is
|
||||||
|
equivalent to C{list(t.iterProcessedJobs(timeout))}. If C{timeout > 0},
|
||||||
|
this is the maximum overall time to spend on collecting processed jobs.
|
||||||
|
'''
|
||||||
|
if timeout is None or timeout <= 0:
|
||||||
|
return list(self.iterProcessedJobs(timeout))
|
||||||
|
now = time.time
|
||||||
|
end = now() + timeout
|
||||||
|
processed = []
|
||||||
|
while timeout > 0:
|
||||||
|
try: processed.append(self.iterProcessedJobs(timeout).next())
|
||||||
|
except StopIteration: break
|
||||||
|
timeout = end - now()
|
||||||
|
return processed
|
||||||
|
|
||||||
|
|
||||||
|
class JobRequest(object):
|
||||||
|
'''A request to execute a callable later and encapsulate its result or
|
||||||
|
exception info.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class UnprocessedRequestError(Exception):
|
||||||
|
'''The callable of a L{JobRequest} has not been called yet.'''
|
||||||
|
|
||||||
|
def __init__(self, callable, args=(), kwds=None, key=None):
|
||||||
|
'''Create a job request for a callable.
|
||||||
|
|
||||||
|
A job request consists of the a callable to be executed by a L{worker
|
||||||
|
thread <Worker>}, a list of positional arguments and a dictionary of
|
||||||
|
keyword arguments.
|
||||||
|
|
||||||
|
@param key: If given, it must be hashable to be used as identifier of
|
||||||
|
the request. It defaults to C{id(self)}.
|
||||||
|
'''
|
||||||
|
if kwds is None: kwds = {}
|
||||||
|
if key is None: key = id(self)
|
||||||
|
for attr in 'callable', 'args', 'kwds', 'key':
|
||||||
|
setattr(self, attr, eval(attr))
|
||||||
|
self._exc_info = None
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
'''Execute the callable of this request with the given arguments and
|
||||||
|
store the result or the raised exception info.
|
||||||
|
'''
|
||||||
|
_log.debug('Ready to process job request %r' % self.key)
|
||||||
|
try:
|
||||||
|
self._result = self.callable(*self.args, **self.kwds)
|
||||||
|
except:
|
||||||
|
self._exc_info = sys.exc_info()
|
||||||
|
_log.debug('Failed to process job request %r' % self.key)
|
||||||
|
else:
|
||||||
|
self._exc_info = None
|
||||||
|
_log.debug('Job request %r was processed successfully' % self.key)
|
||||||
|
|
||||||
|
def result(self):
|
||||||
|
'''Return the computed result for this processed request.
|
||||||
|
|
||||||
|
If the callable had risen an exception, it is reraised here with its
|
||||||
|
original traceback.
|
||||||
|
|
||||||
|
@raise JobRequest.UnprocessedRequestError: If L{process} has not been
|
||||||
|
called for this request.
|
||||||
|
'''
|
||||||
|
if self._exc_info is not None:
|
||||||
|
tp,exception,trace = self._exc_info
|
||||||
|
raise tp,exception,trace
|
||||||
|
try: return self._result
|
||||||
|
except AttributeError:
|
||||||
|
raise self.UnprocessedRequestError
|
||||||
|
|
||||||
|
|
||||||
|
class Worker(threading.Thread):
|
||||||
|
'''Background thread connected to the input/output job request queues.
|
||||||
|
|
||||||
|
A worker thread sits in the background and picks up job requests from one
|
||||||
|
queue and puts the processed requests in another, until it is dismissed.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, inputQueue, outputQueue, unassignedKey2Job, **kwds):
|
||||||
|
'''Set up thread in daemonic mode and start it immediatedly.
|
||||||
|
|
||||||
|
@param inputQueue, outputQueue: U{Queues
|
||||||
|
<http://docs.python.org/lib/module-Queue.html>} passed by the L{ThreadPool}
|
||||||
|
class when it creates a new worker thread.
|
||||||
|
'''
|
||||||
|
super(Worker,self).__init__(**kwds)
|
||||||
|
self.setDaemon(True)
|
||||||
|
self._inputQueue = inputQueue
|
||||||
|
self._outputQueue = outputQueue
|
||||||
|
self._unassignedKey2Job = unassignedKey2Job
|
||||||
|
self.dismissed = False
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
'''Poll the input job queue indefinitely or until told to exit.
|
||||||
|
|
||||||
|
Once a job request has been popped from the input queue, process it and
|
||||||
|
add it to the output queue if it's not cancelled, otherwise discard it.
|
||||||
|
'''
|
||||||
|
while True:
|
||||||
|
# thread blocks here if inputQueue is empty
|
||||||
|
job = self._inputQueue.get()
|
||||||
|
key = job.key
|
||||||
|
_log.debug('Popped job request %r from the input queue' % key)
|
||||||
|
try: del self._unassignedKey2Job[key]
|
||||||
|
except KeyError:
|
||||||
|
_log.info('Discarded cancelled job request %r' % key)
|
||||||
|
continue
|
||||||
|
if self.dismissed: # put back the job we just picked up and exit
|
||||||
|
self._inputQueue.put(job)
|
||||||
|
_log.debug('Dismissing worker %r' % self.getName())
|
||||||
|
break
|
||||||
|
job.process()
|
||||||
|
# thread blocks here if outputQueue is full
|
||||||
|
self._outputQueue.put(job)
|
||||||
|
_log.debug('Added job request %r to the output queue' % job.key)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# demo
|
||||||
|
import random
|
||||||
|
|
||||||
|
# change the seed to get different sequence of results
|
||||||
|
random.seed(2)
|
||||||
|
|
||||||
|
# the work the workers threads will have to do
|
||||||
|
def slow_sqrt(num):
|
||||||
|
t = random.randrange(1,5)
|
||||||
|
log('%s: pretending to work hard on computing sqrt(%s) for %d seconds' %
|
||||||
|
(threading.currentThread().getName(),num,t))
|
||||||
|
time.sleep(t)
|
||||||
|
return num**0.5
|
||||||
|
|
||||||
|
# log each completed job
|
||||||
|
def job_done(job):
|
||||||
|
# job.result() will reraise any exception raised while the job was being
|
||||||
|
# processed; otherwise it will return the computed result
|
||||||
|
try:
|
||||||
|
return 'job #%s: result=%s' % (job.key, job.result())
|
||||||
|
except Exception, ex:
|
||||||
|
return 'job #%s: exception raised: %s' % (job.key, ex)
|
||||||
|
|
||||||
|
def log(msg, start=time.time()):
|
||||||
|
print '%.2f seconds elapsed: %s' % (time.time()-start, msg)
|
||||||
|
|
||||||
|
# create a pool of 3 worker threads
|
||||||
|
pool = ThreadPool(3)
|
||||||
|
|
||||||
|
# create 10 job requests and add them in the queue
|
||||||
|
for i in xrange(10):
|
||||||
|
num = random.randrange(-3,7)
|
||||||
|
pool.addJob(JobRequest(slow_sqrt, [num]))
|
||||||
|
|
||||||
|
# collect all processed jobs within 3.5 seconds
|
||||||
|
firstbatch = pool.processedJobs(timeout=3.5)
|
||||||
|
log('%d jobs done:' % len(firstbatch))
|
||||||
|
for job in firstbatch:
|
||||||
|
print ' ', job_done(job)
|
||||||
|
print '** %d active jobs after first batch' % pool.numActiveJobs()
|
||||||
|
|
||||||
|
# non-blocking iterator over processed jobs
|
||||||
|
for i in xrange(5):
|
||||||
|
for job in pool.iterProcessedJobs(timeout=0):
|
||||||
|
log('From non-blocking loop: %s' % job_done(job))
|
||||||
|
if pool.numActiveJobs():
|
||||||
|
log('Do something in the main thread; will check the pool again after a sec')
|
||||||
|
time.sleep(1)
|
||||||
|
print '** %d active jobs after second batch' % pool.numActiveJobs()
|
||||||
|
|
||||||
|
# blocking iterator over any remaining active jobs
|
||||||
|
for job in pool.iterProcessedJobs():
|
||||||
|
log('From blocking loop: %s' % job_done(job))
|
||||||
|
print '** %d active jobs after third batch' % pool.numActiveJobs()
|
||||||
|
|
||||||
27
src/bunch.py
Normal file
27
src/bunch.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
class Bunch:
|
||||||
|
def __init__(self, **kwds):
|
||||||
|
self.__dict__.update(kwds)
|
||||||
|
|
||||||
|
# that's it! Now, you can create a Bunch
|
||||||
|
# whenever you want to group a few variables:
|
||||||
|
|
||||||
|
point = Bunch(datum=y, squared=y*y, coord=x)
|
||||||
|
|
||||||
|
# and of course you can read/write the named
|
||||||
|
# attributes you just created, add others, del
|
||||||
|
# some of them, etc, etc:
|
||||||
|
if point.squared > threshold:
|
||||||
|
point.isok = 1
|
||||||
|
|
||||||
|
# Even shorter Bunch, Doug Hudgeon, 2001/08/29
|
||||||
|
class Bunch:
|
||||||
|
__init__ = lambda self, **kw: setattr(self, '__dict__', kw)
|
||||||
|
|
||||||
|
|
||||||
|
# When python 2.2 permits the above dynamic __dict__ this could be
|
||||||
|
# be something like:
|
||||||
|
|
||||||
|
class Bunch(list):
|
||||||
|
def __init__(*args, **kw):
|
||||||
|
self[:] = list(args)
|
||||||
|
setattr(self, '__dict__', kw)
|
||||||
217
src/dump_obj.py
Normal file
217
src/dump_obj.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
def printDict(di, format="%-25s %s"):
|
||||||
|
for (key, val) in di.items():
|
||||||
|
print format % (str(key)+':', val)
|
||||||
|
|
||||||
|
def dumpObj(obj, maxlen=77, lindent=24, maxspew=600):
|
||||||
|
"""Print a nicely formatted overview of an object.
|
||||||
|
|
||||||
|
The output lines will be wrapped at maxlen, with lindent of space
|
||||||
|
for names of attributes. A maximum of maxspew characters will be
|
||||||
|
printed for each attribute value.
|
||||||
|
|
||||||
|
You can hand dumpObj any data type -- a module, class, instance,
|
||||||
|
new class.
|
||||||
|
|
||||||
|
Note that in reformatting for compactness the routine trashes any
|
||||||
|
formatting in the docstrings it prints.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> class Foo(object):
|
||||||
|
a = 30
|
||||||
|
def bar(self, b):
|
||||||
|
"A silly method"
|
||||||
|
return a*b
|
||||||
|
... ... ... ...
|
||||||
|
>>> foo = Foo()
|
||||||
|
>>> dumpObj(foo)
|
||||||
|
Instance of class 'Foo' as defined in module __main__ with id 136863308
|
||||||
|
Documentation string: None
|
||||||
|
Built-in Methods: __delattr__, __getattribute__, __hash__, __init__
|
||||||
|
__new__, __reduce__, __repr__, __setattr__,
|
||||||
|
__str__
|
||||||
|
Methods:
|
||||||
|
bar "A silly method"
|
||||||
|
Attributes:
|
||||||
|
__dict__ {}
|
||||||
|
__weakref__ None
|
||||||
|
a 30
|
||||||
|
"""
|
||||||
|
|
||||||
|
import types
|
||||||
|
|
||||||
|
# Formatting parameters.
|
||||||
|
ltab = 2 # initial tab in front of level 2 text
|
||||||
|
|
||||||
|
# There seem to be a couple of other types; gather templates of them
|
||||||
|
MethodWrapperType = type(object().__hash__)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Gather all the attributes of the object
|
||||||
|
#
|
||||||
|
objclass = None
|
||||||
|
objdoc = None
|
||||||
|
objmodule = '<None defined>'
|
||||||
|
|
||||||
|
methods = []
|
||||||
|
builtins = []
|
||||||
|
classes = []
|
||||||
|
attrs = []
|
||||||
|
for slot in dir(obj):
|
||||||
|
attr = getattr(obj, slot)
|
||||||
|
if slot == '__class__':
|
||||||
|
objclass = attr.__name__
|
||||||
|
elif slot == '__doc__':
|
||||||
|
objdoc = attr
|
||||||
|
elif slot == '__module__':
|
||||||
|
objmodule = attr
|
||||||
|
elif (isinstance(attr, types.BuiltinMethodType) or
|
||||||
|
isinstance(attr, MethodWrapperType)):
|
||||||
|
builtins.append( slot )
|
||||||
|
elif (isinstance(attr, types.MethodType) or
|
||||||
|
isinstance(attr, types.FunctionType)):
|
||||||
|
methods.append( (slot, attr) )
|
||||||
|
elif isinstance(attr, types.TypeType):
|
||||||
|
classes.append( (slot, attr) )
|
||||||
|
else:
|
||||||
|
attrs.append( (slot, attr) )
|
||||||
|
|
||||||
|
#
|
||||||
|
# Organize them
|
||||||
|
#
|
||||||
|
methods.sort()
|
||||||
|
builtins.sort()
|
||||||
|
classes.sort()
|
||||||
|
attrs.sort()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Print a readable summary of those attributes
|
||||||
|
#
|
||||||
|
normalwidths = [lindent, maxlen - lindent]
|
||||||
|
tabbedwidths = [ltab, lindent-ltab, maxlen - lindent - ltab]
|
||||||
|
|
||||||
|
def truncstring(s, maxlen):
|
||||||
|
if len(s) > maxlen:
|
||||||
|
return s[0:maxlen] + ' ...(%d more chars)...' % (len(s) - maxlen)
|
||||||
|
else:
|
||||||
|
return s
|
||||||
|
|
||||||
|
# Summary of introspection attributes
|
||||||
|
if objclass == '':
|
||||||
|
objclass = type(obj).__name__
|
||||||
|
intro = "Instance of class '%s' as defined in module %s with id %d" % \
|
||||||
|
(objclass, objmodule, id(obj))
|
||||||
|
print '\n'.join(prettyPrint(intro, maxlen))
|
||||||
|
|
||||||
|
# Object's Docstring
|
||||||
|
if objdoc is None:
|
||||||
|
objdoc = str(objdoc)
|
||||||
|
else:
|
||||||
|
objdoc = ('"""' + objdoc.strip() + '"""')
|
||||||
|
print
|
||||||
|
print prettyPrintCols( ('Documentation string:',
|
||||||
|
truncstring(objdoc, maxspew)),
|
||||||
|
normalwidths, ' ')
|
||||||
|
|
||||||
|
# Built-in methods
|
||||||
|
if builtins:
|
||||||
|
bi_str = delchars(str(builtins), "[']") or str(None)
|
||||||
|
print
|
||||||
|
print prettyPrintCols( ('Built-in Methods:',
|
||||||
|
truncstring(bi_str, maxspew)),
|
||||||
|
normalwidths, ', ')
|
||||||
|
|
||||||
|
# Classes
|
||||||
|
if classes:
|
||||||
|
print
|
||||||
|
print 'Classes:'
|
||||||
|
for (classname, classtype) in classes:
|
||||||
|
classdoc = getattr(classtype, '__doc__', None) or '<No documentation>'
|
||||||
|
print prettyPrintCols( ('',
|
||||||
|
classname,
|
||||||
|
truncstring(classdoc, maxspew)),
|
||||||
|
tabbedwidths, ' ')
|
||||||
|
|
||||||
|
# User methods
|
||||||
|
if methods:
|
||||||
|
print
|
||||||
|
print 'Methods:'
|
||||||
|
for (methodname, method) in methods:
|
||||||
|
methoddoc = getattr(method, '__doc__', None) or '<No documentation>'
|
||||||
|
print prettyPrintCols( ('',
|
||||||
|
methodname,
|
||||||
|
truncstring(methoddoc, maxspew)),
|
||||||
|
tabbedwidths, ' ')
|
||||||
|
|
||||||
|
# Attributes
|
||||||
|
if attrs:
|
||||||
|
print
|
||||||
|
print 'Attributes:'
|
||||||
|
for (attr, val) in attrs:
|
||||||
|
print prettyPrintCols( ('',
|
||||||
|
attr,
|
||||||
|
truncstring(str(val), maxspew)),
|
||||||
|
tabbedwidths, ' ')
|
||||||
|
|
||||||
|
def prettyPrintCols(strings, widths, split=' '):
|
||||||
|
"""Pretty prints text in colums, with each string breaking at
|
||||||
|
split according to prettyPrint. margins gives the corresponding
|
||||||
|
right breaking point."""
|
||||||
|
|
||||||
|
assert len(strings) == len(widths)
|
||||||
|
|
||||||
|
strings = map(nukenewlines, strings)
|
||||||
|
|
||||||
|
# pretty print each column
|
||||||
|
cols = [''] * len(strings)
|
||||||
|
for i in range(len(strings)):
|
||||||
|
cols[i] = prettyPrint(strings[i], widths[i], split)
|
||||||
|
|
||||||
|
# prepare a format line
|
||||||
|
format = ''.join(["%%-%ds" % width for width in widths[0:-1]]) + "%s"
|
||||||
|
|
||||||
|
def formatline(*cols):
|
||||||
|
return format % tuple(map(lambda s: (s or ''), cols))
|
||||||
|
|
||||||
|
# generate the formatted text
|
||||||
|
return '\n'.join(map(formatline, *cols))
|
||||||
|
|
||||||
|
def prettyPrint(string, maxlen=75, split=' '):
|
||||||
|
"""Pretty prints the given string to break at an occurrence of
|
||||||
|
split where necessary to avoid lines longer than maxlen.
|
||||||
|
|
||||||
|
This will overflow the line if no convenient occurrence of split
|
||||||
|
is found"""
|
||||||
|
|
||||||
|
# Tack on the splitting character to guarantee a final match
|
||||||
|
string += split
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
oldeol = 0
|
||||||
|
eol = 0
|
||||||
|
while not (eol == -1 or eol == len(string)-1):
|
||||||
|
eol = string.rfind(split, oldeol, oldeol+maxlen+len(split))
|
||||||
|
lines.append(string[oldeol:eol])
|
||||||
|
oldeol = eol + len(split)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def nukenewlines(string):
|
||||||
|
"""Strip newlines and any trailing/following whitespace; rejoin
|
||||||
|
with a single space where the newlines were.
|
||||||
|
|
||||||
|
Bug: This routine will completely butcher any whitespace-formatted
|
||||||
|
text."""
|
||||||
|
|
||||||
|
if not string: return ''
|
||||||
|
lines = string.splitlines()
|
||||||
|
return ' '.join( [line.strip() for line in lines] )
|
||||||
|
|
||||||
|
def delchars(str, chars):
|
||||||
|
"""Returns a string for which all occurrences of characters in
|
||||||
|
chars have been removed."""
|
||||||
|
|
||||||
|
# Translate demands a mapping string of 256 characters;
|
||||||
|
# whip up a string that will leave all characters unmolested.
|
||||||
|
identity = ''.join([chr(x) for x in range(256)])
|
||||||
|
|
||||||
|
return str.translate(identity, chars)
|
||||||
25
src/excel_table.py
Normal file
25
src/excel_table.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from win32com.client import Dispatch
|
||||||
|
class ExcelTable:
|
||||||
|
"""
|
||||||
|
display a data structure in Excel
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.xlApp = Dispatch("Excel.Application")
|
||||||
|
self.xlApp.Visible = 1
|
||||||
|
self.xlApp.Workbooks.Add()
|
||||||
|
|
||||||
|
def Display(self,InputList,StartRow=1,StartColumn=1):
|
||||||
|
"""
|
||||||
|
Display a Grid of data in excel
|
||||||
|
Input list = List of (row,col,value) to describe your data
|
||||||
|
StartRow,StartColumn - the place where to start to draw you table in excel.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.List=InputList
|
||||||
|
for x in self.List:
|
||||||
|
self.xlApp.ActiveSheet.Cells(x[0]+StartRow,x[1]+StartColumn).Value =str(x[2])
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.xlApp.ActiveWorkbook.Close(SaveChanges=0)
|
||||||
|
self.xlApp.Quit()
|
||||||
|
del self.xlApp
|
||||||
109
src/yaptu.py
Normal file
109
src/yaptu.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"Yet Another Python Templating Utility, Version 1.2"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# utility stuff to avoid tests in the mainline code
|
||||||
|
class _nevermatch:
|
||||||
|
"Polymorphic with a regex that never matches"
|
||||||
|
def match(self, line):
|
||||||
|
return None
|
||||||
|
_never = _nevermatch() # one reusable instance of it suffices
|
||||||
|
def identity(string, why):
|
||||||
|
"A do-nothing-special-to-the-input, just-return-it function"
|
||||||
|
return string
|
||||||
|
def nohandle(string):
|
||||||
|
"A do-nothing handler that just re-raises the exception"
|
||||||
|
raise
|
||||||
|
|
||||||
|
# and now the real thing
|
||||||
|
class copier:
|
||||||
|
"Smart-copier (YAPTU) class"
|
||||||
|
def copyblock(self, i=0, last=None):
|
||||||
|
"Main copy method: process lines [i,last) of block"
|
||||||
|
def repl(match, self=self):
|
||||||
|
"return the eval of a found expression, for replacement"
|
||||||
|
# uncomment for debug: print '!!! replacing',match.group(1)
|
||||||
|
expr = self.preproc(match.group(1), 'eval')
|
||||||
|
try: return str(eval(expr, self.globals, self.locals))
|
||||||
|
except: return str(self.handle(expr))
|
||||||
|
block = self.locals['_bl']
|
||||||
|
if last is None: last = len(block)
|
||||||
|
while i<last:
|
||||||
|
line = block[i]
|
||||||
|
match = self.restat.match(line)
|
||||||
|
if match: # a statement starts "here" (at line block[i])
|
||||||
|
# i is the last line to _not_ process
|
||||||
|
stat = match.string[match.end(0):].strip()
|
||||||
|
j=i+1 # look for 'finish' from here onwards
|
||||||
|
nest=1 # count nesting levels of statements
|
||||||
|
while j<last:
|
||||||
|
line = block[j]
|
||||||
|
# first look for nested statements or 'finish' lines
|
||||||
|
if self.restend.match(line): # found a statement-end
|
||||||
|
nest = nest - 1 # update (decrease) nesting
|
||||||
|
if nest==0: break # j is first line to _not_ process
|
||||||
|
elif self.restat.match(line): # found a nested statement
|
||||||
|
nest = nest + 1 # update (increase) nesting
|
||||||
|
elif nest==1: # look for continuation only at this nesting
|
||||||
|
match = self.recont.match(line)
|
||||||
|
if match: # found a contin.-statement
|
||||||
|
nestat = match.string[match.end(0):].strip()
|
||||||
|
stat = '%s _cb(%s,%s)\n%s' % (stat,i+1,j,nestat)
|
||||||
|
i=j # again, i is the last line to _not_ process
|
||||||
|
j=j+1
|
||||||
|
stat = self.preproc(stat, 'exec')
|
||||||
|
stat = '%s _cb(%s,%s)' % (stat,i+1,j)
|
||||||
|
# for debugging, uncomment...: print "-> Executing: {"+stat+"}"
|
||||||
|
exec stat in self.globals,self.locals
|
||||||
|
i=j+1
|
||||||
|
else: # normal line, just copy with substitution
|
||||||
|
self.ouf.write(self.regex.sub(repl,line))
|
||||||
|
i=i+1
|
||||||
|
def __init__(self, regex=_never, dict={},
|
||||||
|
restat=_never, restend=_never, recont=_never,
|
||||||
|
preproc=identity, handle=nohandle, ouf=sys.stdout):
|
||||||
|
"Initialize self's attributes"
|
||||||
|
self.regex = regex
|
||||||
|
self.globals = dict
|
||||||
|
self.locals = { '_cb':self.copyblock }
|
||||||
|
self.restat = restat
|
||||||
|
self.restend = restend
|
||||||
|
self.recont = recont
|
||||||
|
self.preproc = preproc
|
||||||
|
self.handle = handle
|
||||||
|
self.ouf = ouf
|
||||||
|
def copy(self, block=None, inf=sys.stdin):
|
||||||
|
"Entry point: copy-with-processing a file, or a block of lines"
|
||||||
|
if block is None: block = inf.readlines()
|
||||||
|
self.locals['_bl'] = block
|
||||||
|
self.copyblock()
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
"Test: copy a block of lines, with full processing"
|
||||||
|
import re
|
||||||
|
rex=re.compile('@([^@]+)@')
|
||||||
|
rbe=re.compile('\+')
|
||||||
|
ren=re.compile('-')
|
||||||
|
rco=re.compile('= ')
|
||||||
|
x=23 # just a variable to try substitution
|
||||||
|
cop = copier(rex, globals(), rbe, ren, rco)
|
||||||
|
lines_block = [line+'\n' for line in """
|
||||||
|
A first, plain line -- it just gets copied.
|
||||||
|
A second line, with @x@ substitutions.
|
||||||
|
+ x+=1 # non-block statements MUST end with comments
|
||||||
|
-
|
||||||
|
Now the substitutions are @x@.
|
||||||
|
+ if x>23:
|
||||||
|
After all, @x@ is rather large!
|
||||||
|
= else:
|
||||||
|
After all, @x@ is rather small!
|
||||||
|
-
|
||||||
|
+ for i in range(3):
|
||||||
|
Also, @i@ times @x@ is @i*x@.
|
||||||
|
-
|
||||||
|
One last, plain line at the end.""".split('\n')]
|
||||||
|
print "*** input:"
|
||||||
|
print ''.join(lines_block)
|
||||||
|
print "*** output:"
|
||||||
|
cop.copy(lines_block)
|
||||||
|
|
||||||
Reference in New Issue
Block a user