documentation

This commit is contained in:
Michael Davidsaver
2013-04-01 09:16:59 -04:00
parent 2cc7c7ce26
commit 4facd4339a
10 changed files with 1105 additions and 23 deletions

View File

@@ -15,13 +15,23 @@ class _Record(object):
def __init__(self, rec):
pass
def name(self):
"""Record name
"""Record name string.
>>> R = getRecord("my:record:name")
>>> R.name()
"my:record:name"
"""
def rtype(self):
"""Record type
"""Record type name string.
>>> R = getRecord("my:record:name")
>>> R.type()
"longin"
"""
def isPyRecord(self):
"""Is this record using Python Device.
"""Is this record using Python device support.
:rtype: bool
"""
def info(self, key):
"""info(key)
@@ -37,17 +47,70 @@ class _Record(object):
"""
def scan(self, sync=False, reason=None, force=0):
"""scan(sync=False)
"""Scan this record.
Scan this record. If sync is False then a
scan request is queued. If sync is True then the record
:param sync: scan in current thread (``True``), or queue (``False``).
:param reason: Reason object passed to :meth:`process <DeviceSupport.process>` (sync=True only)
:param force: Record processing condtion (0=Passive, 1=Force, 2=I/O Intr)
:throws: ``RuntimeError`` when ``sync=True``, but ``force`` prevents scanning.
If ``sync`` is False then a
scan request is queued to run in another thread..
If ``sync`` is True then the record
is scannined immidately on the current thread.
For ``reason`` argument must be used in conjunction with ``sync=True``
on records with Python device support. This provides a means
of providing extra contextual information to the record's
:meth:`process <DeviceSupport.process>` method.
``force`` is used to decide if the record will actuall be processed,
``force=0`` will only process records with SCAN=Passive.
``force=1`` will process any record if at all possible.
``force=2`` will only process records with Python device support and
SCAN=I/O Intr.
It is **never** safe to use ``sync=True`` while holding record locks,
including from within a *process* method.
"""
def asyncStart(self):
pass
"""Start asynchronous processing
This method may be called from a device support
:meth:`process <DeviceSupport.process>` method
to indicate that processing will continue
later.
.. important::
This method is **only** safe to call within a *process* method.
"""
def asyncFinish(self, reason=None):
pass
"""Indicate that asynchronous processing can complete
Similar to :meth:`scan`. Used to conclude asynchronous
process started with :meth:`asyncStart`.
Processing is completed on the current thread.
.. important::
This method should **never** be called within
a :meth:`process <DeviceSupport.process>` method,
or any other context where a Record lock is held.
Doing so will result in a deadlock.
Typically a *reason* will be passed to *process* as a way
of indicating that this is the completion of an async action. ::
AsyncDone = object()
class MySup(object):
def process(record, reason):
if reason is AsyncDone:
record.VAL = ... # store result
else:
threading.Timer(1.0, record.asyncFinish, kwargs={'reason':AsyncDone})
record.asyncStart()
"""
class _Field(object):
"""Handle for field operations
@@ -59,7 +122,11 @@ class _Field(object):
def __init__(self, fld):
pass
def name(self):
"""("rec", "FLD") = name()
"""Fetch the record and field names.
>>> FLD = getRecord("rec").field("FLD")
>>> FLD.name()
("rec", "FLD")
"""
def fieldinfo(self):
"""(type, size, #elements) = fieldinfo()
@@ -72,17 +139,29 @@ class _Field(object):
def getval(self):
"""Fetch the current field value as a scalar.
Returns Int, Float, or str
:rtype: int, float, or str
Returned type depends of field DBF type.
An ``int`` is returned for CHAR, SHORT, LONG, and ENUM.
A ``float`` is returned for FLOAT and DOUBLE.
A ``str`` is returned for STRING.
"""
def putval(self, val):
"""Update the field value
Must be an Int, Float or str
Must be an Int, Float or str. Strings will be truncated to 39 charactors.
"""
def getarray(self):
"""Return a numpy ndarray refering to this field for in-place operations.
The dtype of the ndarray will correspond to the field's DBF type.
Its size will be the **maximum** number of elements.
.. important::
It is only safe to read or write to this ndarray while the record
lock is held (ie withing :meth:`process <DeviceSupport.process>`).
"""
_hooks = {}

View File

@@ -19,6 +19,15 @@ __all__ = [
]
def getRecord(name):
"""Retrieve a :class:`Record` instance by the
full record name.
The result is cached so the future calls will return the same instance.
This is the prefered way to get :class:`Record` instances.
>>> R = getRecord("my:record:name")
Record("my:record:name")
"""
try:
return _rec_cache[name]
except KeyError:
@@ -27,12 +36,45 @@ def getRecord(name):
return rec
class IOScanListBlock(object):
"""A list of records which will be processed together.
This convienence class to handle the accounting to
maintain a list of records.
"""
def __init__(self):
super(IOScanListBlock,self).__init__()
self._recs, self._recs_add, self._recs_remove = set(), set(), set()
self.force, self._running = 2, False
def add(self, rec):
"""Add a record to the scan list.
This method is designed to be consistent
with :meth:`allowScan <DeviceSupport.allowScan>`
by returning its :meth:`remove` method.
If fact this function can be completely delegated. ::
class MyDriver(util.StoppableThread):
def __init__(self):
super(MyDriver,self).__init__()
self.lock = threading.Lock()
self.scan1 = IOScanListBlock()
def run(self):
try:
while self.shouldRun():
time.sleep(1)
with self.lock:
self.scan1.interrupt()
finally:
self.finish()
class MySup(object):
def __init__(self, driver):
self.driver = driver
def allowScan(rec):
with self.driver.lock:
return self.driver.scan1.add(rec)
"""
assert isinstance(rec, Record)
if self._running:
@@ -45,6 +87,8 @@ class IOScanListBlock(object):
return self.remove
def remove(self, rec):
"""Remove a record from the scan list.
"""
if self._running:
self._recs_add.discard(rec)
self._recs_add._recs_remove(rec)
@@ -53,6 +97,15 @@ class IOScanListBlock(object):
self._recs.discard(rec)
def interrupt(self, reason=None, mask=None):
"""Scan the records in this list.
:param reason: Passed to :meth:`Record.scan`.
:param mask: A *list* or *set* or records which should not be scanned.
This method will call :meth:`Record.scan` of each of the records
currently in the list. This is done synchronously in the current
thread. It should **never** be call when any record locks are held.
"""
self._running = True
try:
for R in self._recs:
@@ -75,6 +128,10 @@ def _default_whendone(type, val, tb):
traceback.print_exception(type, val, tb)
class IOScanListThread(IOScanListBlock):
"""A list of records w/ a worker thread to run them.
All methods are thread-safe.
"""
_worker = None
_worker_lock = threading.Lock()
queuelength=100
@@ -95,6 +152,28 @@ class IOScanListThread(IOScanListBlock):
self._lock = threading.Lock()
def add(self, rec):
"""Add a record to the scan list.
This method is thread-safe and may be used
without additional locking. ::
class MyDriver(util.StoppableThread):
def __init__(self):
super(MyDriver,self).__init__()
self.scan1 = IOScanListThread()
def run(self):
try:
while self.shouldRun():
time.sleep(1)
self.scan1.interrupt()
finally:
self.finish()
class MySup(object):
def __init__(self, driver):
self.driver = driver
self.allowScan = self.driver.scan1.add
"""
with self._lock:
return super(IOScanListThread,self).add(rec)
@@ -103,12 +182,26 @@ class IOScanListThread(IOScanListBlock):
return super(IOScanListThread,self).remove(rec)
def interrupt(self, reason=None, mask=None, whendone=_default_whendone):
"""Queue a request to process the scan list.
:param reason: Passed to :meth:`Record.scan`.
:param mask: A *list* or *set* or records which should not be scanned.
:param whendone: A callable which will be invoked after all records are processed.
:throws: RuntimeError is the request can't be queued.
Calling this method will cause a request to be sent to a
worker thread. This method can be called several times
to queue several requests.
If provided, the *whendone* callable is invoked with three arguments.
These will be None except in the case an interrupt is raised in the
worker in which case they are: exception type, value, and traceback.
.. note::
This method may be safely called while record locks are held.
"""
W = self.getworker()
try:
W.add(self._X, (reason, mask, whendone))
return True
except RuntimeError:
return False
W.add(self._X, (reason, mask, whendone))
def _X(self, reason, mask, whendone):
try:
@@ -127,18 +220,26 @@ class Record(_dbapi._Record):
def field(self, name):
"""Lookup field in this record
fld = rec.field('HOPR')
:rtype: :class:`Field`
:throws: KeyError for non-existant fields.
The returned object is cached so future calls will
return the same instance.
>>> getRecord("rec").field('HOPR')
Field("rec.HOPR")
"""
try:
F = self._fld_cache[name]
if F is _no_such_field:
raise ValueError()
return F
except KeyError:
except KeyError, e:
try:
fld = Field("%s.%s"%(self.name(), name))
except ValueError:
self._fld_cache[name] = _no_such_field
raise e
else:
self._fld_cache[name] = fld
return fld
@@ -166,7 +267,7 @@ class Record(_dbapi._Record):
class Field(_dbapi._Field):
@property
def record(self):
"""Fetch the record associated with this field
"""Fetch the :class:`Record` associated with this field
"""
try:
return self._record

View File

@@ -13,11 +13,17 @@ hooknames = _dbapi._hooks.keys()
def addHook(state, func):
"""addHook("stats", funcion)
Add callback function to IOC start sequence.
Add callable to IOC start sequence.
def show():
print 'State Occurred'
addHook("AfterIocRunning", show)
Callables are run in the reverse of the order in
which they were added.
>>> def show():
... print 'State Occurred'
>>> addHook("AfterIocRunning", show)
An additional special hook 'AtIocExit' may be used
for cleanup actions during IOC shutdown.
"""
sid = _dbapi._hooks[state]
try: