diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d66bc01 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +sudo: false +language: python +python: + - "2.6" + - "2.7" + - "3.3" + - "3.4" + - "3.5" + - "3.6" + + + +# Get epics base +addons: + apt: + sources: + - sourceline: 'deb http://epics.nsls2.bnl.gov/debian/ wheezy main contrib' + key_url: 'http://epics.nsls2.bnl.gov/debian/repo-key.pub' + + packages: + - epics-dev + - build-essential + +env: +- EPICS_BASE=/usr/lib/epics EPICS_HOST_ARCH=linux-x86_64 + +cache: + directories: + - $HOME/.cache/pip + - ${VIRTUAL_ENV}/lib/python${TRAVIS_PYTHON_VERSION}/site-packages + - ${VIRTUAL_ENV}/bin + +install: + - env + - ls -al ${VIRTUAL_ENV}/lib/python${TRAVIS_PYTHON_VERSION}/site-packages + - ls -al ${VIRTUAL_ENV}/bin + - pip install numpy pytest + - pip install pytest-cov coveralls + - ls -al ${VIRTUAL_ENV}/lib/python${TRAVIS_PYTHON_VERSION}/site-packages + - ls -al ${VIRTUAL_ENV}/bin + - make PYTHON=python dist build_ext + +# command to run tests +script: + - py.test --cov=cothread --tb=native -vv tests + +# submit coverage +after_script: + - coveralls diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f3762ce --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include context/* +include README.rst + diff --git a/Makefile b/Makefile index d6331df..4a15af6 100644 --- a/Makefile +++ b/Makefile @@ -31,9 +31,20 @@ install: dist --install-dir=$(INSTALL_DIR) \ --script-dir=$(SCRIPT_DIR) dist/*.egg +# publish +publish: default + $(PYTHON) setup.py sdist upload -r pypi + +# publish to test pypi +testpublish: default + $(PYTHON) setup.py sdist upload -r pypitest + docs: cothread/_coroutine.so sphinx-build -b html docs docs/html +test: + $(PYTHON) setup.py test + clean_docs: rm -rf docs/html @@ -46,3 +57,6 @@ cothread/libca_path.py: cothread/_coroutine.so: $(wildcard context/*.c context/*.h) $(PYTHON) setup.py build_ext -i + +build_ext: cothread/_coroutine.so +.PHONY: build_ext diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..25a9f3d --- /dev/null +++ b/README.rst @@ -0,0 +1,50 @@ +|build_status| |coverage| |pypi-version| |readthedocs| + +cothread +======== + +The `cothread` Python library is designed for building tools using cooperative +threading. This means that, with care, programs can effectively run several +tasks simultaneously. + +The `cothread.catools` library is designed to support easy channel access from +Python, and makes essential use of the features of cooperative threads -- in +particular, `catools.camonitor()` notifies updates in the background. + +See the documentation for more details. + + +Installation +------------ +To install the latest release, type:: + + pip install cothread + +To install the latest code directly from source, type:: + + pip install git+git://github.com/dls-controls/cothread + +Documentation +============= + +Full documentation is available at http://cothread.readthedocs.org + +License +======= +GPL2 License (see COPYING) + +.. |pypi-version| image:: https://img.shields.io/pypi/v/cothread.svg + :target: https://pypi.python.org/pypi/cothread/ + :alt: Latest PyPI version + +.. |readthedocs| image:: https://readthedocs.org/projects/cothread/badge/?version=latest + :target: https://readthedocs.org/projects/cothread/?badge=latest + :alt: Documentation Status + +.. |build_status| image:: https://travis-ci.org/dls-controls/cothread.svg?style=flat + :target: https://travis-ci.org/dls-controls/cothread + :alt: Build Status + +.. |coverage| image:: https://coveralls.io/repos/dls-controls/cothread/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/dls-controls/cothread?branch=master + :alt: Test coverage diff --git a/context/_coroutine.c b/context/_coroutine.c index 65f2fc5..d7f045b 100644 --- a/context/_coroutine.c +++ b/context/_coroutine.c @@ -78,16 +78,34 @@ static int get_cocore(PyObject *object, void **result) static void *coroutine_wrapper(void *action_, void *arg_) { PyThreadState *thread_state = PyThreadState_GET(); + /* New coroutine gets a brand new Python interpreter stack frame. */ thread_state->frame = NULL; thread_state->recursion_depth = 0; + /* Also reset the exception state in case it's non NULL at this point. We + * don't own these pointers at this point, coroutine_switch does. */ + thread_state->exc_type = NULL; + thread_state->exc_value = NULL; + thread_state->exc_traceback = NULL; + /* Call the given action with the passed argument. */ PyObject *action = *(PyObject **)action_; PyObject *arg = arg_; PyObject *result = PyObject_CallFunctionObjArgs(action, arg, NULL); Py_DECREF(action); Py_DECREF(arg); + + /* Some of the stuff we've initialised can leak through, so far I've only + * seen exc_type still set at this point, but maybe other fields can also + * leak. Avoid a memory leak by making sure we're not holding onto these. + * All these pointers really are defunct, because as soon as we return + * coroutine_switch will replace all these values. */ + Py_XDECREF(thread_state->frame); + Py_XDECREF(thread_state->exc_type); + Py_XDECREF(thread_state->exc_value); + Py_XDECREF(thread_state->exc_traceback); + return result; } @@ -126,6 +144,14 @@ static PyObject *coroutine_switch(PyObject *Self, PyObject *args) struct _frame *python_frame = thread_state->frame; int recursion_depth = thread_state->recursion_depth; + /* We also need to switch the exception state around: if we don't do + * this then we get confusion about the lifetime of exception state + * between coroutines. The most obvious problem is that the exception + * isn't properly cleared on function return. */ + PyObject *exc_type = thread_state->exc_type; + PyObject *exc_value = thread_state->exc_value; + PyObject *exc_traceback = thread_state->exc_traceback; + /* Switch to new coroutine. For the duration arg needs an extra * reference count, it'll be accounted for either on the next returned * result or in the entry to a new coroutine. */ @@ -137,6 +163,11 @@ static PyObject *coroutine_switch(PyObject *Self, PyObject *args) thread_state = PyThreadState_GET(); thread_state->frame = python_frame; thread_state->recursion_depth = recursion_depth; + + /* Restore the exception state. */ + thread_state->exc_type = exc_type; + thread_state->exc_value = exc_value; + thread_state->exc_traceback = exc_traceback; return result; } else @@ -157,6 +188,21 @@ static PyObject *coroutine_getcurrent(PyObject *self, PyObject *args) } +static PyObject *coroutine_is_equal(PyObject *self, PyObject *args) +{ + struct cocore *cocore1, *cocore2; + if (PyArg_ParseTuple(args, "O&O&", get_cocore, &cocore1, get_cocore, &cocore2)) + { + if (cocore1 == cocore2) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; + } + else + return NULL; +} + + static PyObject *enable_check_stack(PyObject *self, PyObject *arg) { int is_true = PyObject_IsTrue(arg); @@ -228,6 +274,9 @@ static PyObject *install_readline_hook(PyObject *self, PyObject *arg) static PyMethodDef module_methods[] = { { "get_current", coroutine_getcurrent, METH_NOARGS, "_coroutine.getcurrent()\nReturns the current coroutine." }, + { "is_equal", coroutine_is_equal, METH_VARARGS, + "is_equal(coroutine1, coroutine2)\n\ +Compares two coroutine objects for equality" }, { "create", coroutine_create, METH_VARARGS, "create(parent, action, stack_size)\n\ Creates a new coroutine with the given action to invoke. The parent\n\ diff --git a/context/switch-arm.c b/context/switch-arm.c index f4bb5ba..15ef951 100644 --- a/context/switch-arm.c +++ b/context/switch-arm.c @@ -29,6 +29,14 @@ // Coroutine frame switching for ARM +// If Vector Floating Point support is present then we need to preserve the VFP +// registers d8-d15. +#ifdef __VFP_FP__ +#define IF_VFP_FP(code) code +#else +#define IF_VFP_FP(code) +#endif + __asm__( " .text\n" " .align 2\n" @@ -41,9 +49,13 @@ __asm__( FNAME(switch_frame) " stmfd sp!, {r4, r5, r6, r7, r8, r9, sl, fp, lr}\n" +IF_VFP_FP( +" fstmfdd sp!, {d8-d15}\n") " str sp, [r0]\n" " mov sp, r1\n" " mov r0, r2\n" +IF_VFP_FP( +" fldmfdd sp!, {d8-d15}\n") " ldmfd sp!, {r4, r5, r6, r7, r8, r9, sl, fp, pc}\n" FSIZE(switch_frame) @@ -58,6 +70,8 @@ FNAME(create_frame) " mov ip, lr\n" // Save LR so can use same STM slot " ldr lr, =action_entry\n" " stmfd r0!, {r4, r5, r6, r7, r8, r9, sl, fp, lr}\n" +IF_VFP_FP( +" fstmfdd r0!, {d8-d15}\n") " bx ip\n" "action_entry:\n" diff --git a/context/tests/coco.py b/context/tests/coco.py index 4464d9b..d86bf08 100755 --- a/context/tests/coco.py +++ b/context/tests/coco.py @@ -1,4 +1,4 @@ -#!/dls_sw/tools/python2.4-debug/bin/python2.4 +#!/usr/bin/env python from __future__ import print_function diff --git a/context/tests/leak.py b/context/tests/leak.py index 8c886dd..ac0c735 100755 --- a/context/tests/leak.py +++ b/context/tests/leak.py @@ -1,4 +1,4 @@ -#!/dls_sw/tools/python2.4-debug/bin/python2.4 +#!/usr/bin/env python from __future__ import print_function diff --git a/cothread/__init__.py b/cothread/__init__.py index 1e044ad..e0a8018 100644 --- a/cothread/__init__.py +++ b/cothread/__init__.py @@ -45,6 +45,7 @@ from .input_hook import * from .coselect import * from .cosocket import * +from .version import __version__ # Publish all public symbols from cothread and input_hook as default exports. # The coselect functions aren't exported by default but are available. diff --git a/cothread/cadef.py b/cothread/cadef.py index 6651b00..8d98fa9 100644 --- a/cothread/cadef.py +++ b/cothread/cadef.py @@ -47,6 +47,7 @@ import ctypes from .load_ca import libca +from . import py23 @@ -192,6 +193,7 @@ def convert_py_object(object, function, args): ca_message = libca.ca_message ca_message.argtypes = [ctypes.c_long] ca_message.restype = ctypes.c_char_p +ca_message.errcheck = py23.auto_decode # channel_name = ca_name(channel) @@ -200,6 +202,7 @@ def convert_py_object(object, function, args): ca_name = libca.ca_name ca_name.argtypes = [ctypes.c_void_p] ca_name.restype = ctypes.c_char_p +ca_name.errcheck = py23.auto_decode # @exception_handler @@ -243,7 +246,7 @@ def convert_py_object(object, function, args): # recovered by calling ca_puser(channel_id). ca_create_channel = libca.ca_create_channel ca_create_channel.argtypes = [ - ctypes.c_char_p, connection_handler, ctypes.py_object, + py23.auto_encode, connection_handler, ctypes.py_object, ctypes.c_int, ctypes.c_void_p] ca_create_channel.errcheck = expect_ECA_NORMAL @@ -397,6 +400,7 @@ def convert_py_object(object, function, args): ca_host_name = libca.ca_host_name ca_host_name.argtypes = [ctypes.c_void_p] ca_host_name.restype = ctypes.c_char_p +ca_host_name.errcheck = py23.auto_decode # read = ca_read_access(channel_id) diff --git a/cothread/catools.py b/cothread/catools.py index 6ea9586..f8bdf11 100644 --- a/cothread/catools.py +++ b/cothread/catools.py @@ -58,6 +58,7 @@ from . import cothread from . import cadef from . import dbr +from . import py23 from .dbr import * from .cadef import * @@ -68,6 +69,7 @@ 'caget', # Read PVs from channel access 'camonitor', # Monitor PVs over channel access 'connect', # Establish PV connection + 'cainfo', # Returns ca_info describing PV connection ] + dbr.__all__ + cadef.__all__ @@ -84,6 +86,11 @@ def _check_env(name, default): CA_ACTION_STACK = _check_env('CATOOLS_ACTION_STACK', 0) +if sys.version_info < (3,): + pv_string_types = (str, unicode) +else: + pv_string_types = str + class ca_nothing(Exception): '''This value is returned as a success or failure indicator from caput, @@ -102,8 +109,9 @@ def __repr__(self): def __str__(self): return '%s: %s' % (self.name, cadef.ca_message(self.errorcode)) - def __nonzero__(self): + def __bool__(self): return self.ok + __nonzero__ = __bool__ # For python 2 def __iter__(self): '''This is *not* supposed to be an iterable object, but the base class @@ -145,8 +153,8 @@ def ca_timeout(event, timeout, name): ca_nothing timeout exception containing the PV name.''' try: return event.Wait(timeout) - except cothread.Timedout: - raise ca_nothing(name, cadef.ECA_TIMEOUT) + except cothread.Timedout as timeout: + py23.raise_from(ca_nothing(name, cadef.ECA_TIMEOUT), timeout) # ---------------------------------------------------------------------------- @@ -550,7 +558,7 @@ def camonitor(pvs, callback, **kargs): if notify_disconnect is False, and that if the PV subsequently connects it will update as normal. ''' - if isinstance(pvs, str): + if isinstance(pvs, pv_string_types): return _Subscription(pvs, callback, **kargs) else: return [ @@ -733,7 +741,7 @@ def caget(pvs, **kargs): The format of values returned depends on the number of values requested for each PV. If only one value is requested then the value is returned as a scalar, otherwise as a numpy array.''' - if isinstance(pvs, str): + if isinstance(pvs, pv_string_types): return caget_one(pvs, **kargs) else: return caget_array(pvs, **kargs) @@ -806,7 +814,7 @@ def caput_one(pv, value, datatype=None, wait=False, timeout=5, callback=None): def caput_array(pvs, values, repeat_value=False, **kargs): # Bring the arrays of pvs and values into alignment. - if repeat_value or isinstance(values, str): + if repeat_value or isinstance(values, pv_string_types): # If repeat_value is requested or the value is a string then we treat # it as a single value. values = [values] * len(pvs) @@ -877,7 +885,7 @@ def caput(pvs, values, **kargs): If caput completed succesfully then .ok is True and .name is the corresponding PV name. If throw=False was specified and a put failed then .errorcode is set to the appropriate ECA_ error code.''' - if isinstance(pvs, str): + if isinstance(pvs, pv_string_types): return caput_one(pvs, values, **kargs) else: return caput_array(pvs, values, **kargs) @@ -982,12 +990,18 @@ def connect(pvs, **kargs): connected to. If this is set to False then instead for each failing PV a sentinel value with .ok == False is returned. ''' - if isinstance(pvs, str): + if isinstance(pvs, pv_string_types): return connect_one(pvs, **kargs) else: return connect_array(pvs, **kargs) +def cainfo(pvs, **args): + '''Returns a ca_info structure for the given PVs. See the documentation + for connect() for more detail.''' + return connect(pvs, cainfo = True, wait = True, **args) + + # ---------------------------------------------------------------------------- # Final module initialisation @@ -1038,13 +1052,3 @@ def __call__(self): self._flush_io_event.Signal() _flush_io = _FlushIo() - - -# The value of the exception handler below is rather doubtful... -if False: - @exception_handler - def catools_exception(args): - '''print ca exception message''' - print('catools_exception:', args.ctx, cadef.ca_message(args.stat), - file = sys.stderr) - cadef.ca_add_exception_event(catools_exception, 0) diff --git a/cothread/coselect.py b/cothread/coselect.py index b87d804..d3e4801 100644 --- a/cothread/coselect.py +++ b/cothread/coselect.py @@ -68,7 +68,7 @@ def select_hook(): # A helpful routine to ensure that our select() behaves as much as possible # like the real thing! def _AsFileDescriptor(file): - if isinstance(file, (int, long)): + if isinstance(file, int): return file else: return file.fileno() @@ -248,6 +248,7 @@ def poll_list(event_list, timeout = None): constants). This routine will cooperatively block until any descriptor signals a selected event (or any event from HUP, ERR, NVAL) or until the timeout (in seconds) occurs.''' + cothread.cothread._validate_thread() poller = _Poller(event_list) cothread.cothread._scheduler.poll_until( poller, cothread.cothread.GetDeadline(timeout)) diff --git a/cothread/coserver.py b/cothread/coserver.py index cca8edc..07d0090 100644 --- a/cothread/coserver.py +++ b/cothread/coserver.py @@ -22,11 +22,20 @@ from the SocketServer and BaseHTTPServer modules """ -import SocketServer, BaseHTTPServer, SimpleHTTPServer +import sys -import cothread -import cosocket -import coselect +if sys.version_info < (3,): + from SocketServer import BaseServer, TCPServer, UDPServer, ThreadingMixIn + from BaseHTTPServer import HTTPServer, test as _test + from SimpleHTTPServer import SimpleHTTPRequestHandler + +else: + from socketserver import BaseServer, TCPServer, UDPServer, ThreadingMixIn + from http.server import HTTPServer, SimpleHTTPRequestHandler, test as _test + +from . import cothread +from . import cosocket +from . import coselect __all__ = [ 'BaseServer', @@ -60,7 +69,7 @@ def __init__(self, *args, **kws): self.__shut = cosocket.socketpair() if hasattr(cls, 'address_family'): - self.socket = cosocket.socket(None, None, None, self.socket) + self.socket = cosocket.cosocket(_sock = self.socket) if baact: self.server_bind() self.server_activate() @@ -101,13 +110,13 @@ def server_close(self): return WrappedServer -BaseServer = _patch(SocketServer.BaseServer) -TCPServer = _patch(SocketServer.TCPServer) -UDPServer = _patch(SocketServer.UDPServer) -HTTPServer = _patch(BaseHTTPServer.HTTPServer) +BaseServer = _patch(BaseServer) +TCPServer = _patch(TCPServer) +UDPServer = _patch(UDPServer) +HTTPServer = _patch(HTTPServer) -class CoThreadingMixIn(SocketServer.ThreadingMixIn): +class CoThreadingMixIn(ThreadingMixIn): def process_request(self, request, client_address): cothread.Spawn(self.process_request_thread, request, client_address) @@ -115,10 +124,9 @@ class CoThreadingUDPServer(CoThreadingMixIn, UDPServer): pass class CoThreadingTCPServer(CoThreadingMixIn, TCPServer): pass class CoThreadingHTTPServer(CoThreadingMixIn, HTTPServer): pass -def test(HandlerClass = SimpleHTTPServer.SimpleHTTPRequestHandler, - ServerClass = CoThreadingHTTPServer): - BaseHTTPServer.test(HandlerClass, ServerClass) - +def test(HandlerClass=SimpleHTTPRequestHandler, + ServerClass=CoThreadingHTTPServer): + _test(HandlerClass, ServerClass) if __name__ == '__main__': test() diff --git a/cothread/cosocket.py b/cothread/cosocket.py index da3545d..0252de2 100644 --- a/cothread/cosocket.py +++ b/cothread/cosocket.py @@ -30,6 +30,7 @@ standard socket module.''' import os +import sys import errno from . import coselect @@ -48,33 +49,44 @@ def socket_hook(): '''Replaces the blocking methods in the socket module with the non-blocking methods implemented here. Not safe to call if other threads need the original methods.''' - _socket.socket = socket + _socket.socket = cosocket _socket.socketpair = socketpair + def socketpair(*args): - # For unfathomable reasons socketpair() returns un-wrapped '_socket.socket' - # So they are only wrapped once. - return tuple(map(lambda S: socket(_sock = S), _socket_pair(*args))) -socketpair.__doc__ = _socket.socketpair.__doc__ + a, b = _socket_pair(*args) + # Now wrap them to make them co-operative if needed + if not isinstance(a, cosocket): + a = cosocket(_sock = a) + if not isinstance(b, cosocket): + b = cosocket(_sock = b) + return a, b +socketpair.__doc__ = _socket_pair.__doc__ + def create_connection(*args, **kargs): sock = _socket.create_connection(*args, **kargs) - return socket(_sock = sock) + return cosocket(_sock = sock) create_connection.__doc__ = _socket.create_connection.__doc__ -class socket(object): - __doc__ = _socket_socket.__doc__ - def wrap(fun): - fun.__doc__ = getattr(_socket_socket, fun.__name__).__doc__ - return fun +def wrap(fun): + fun.__doc__ = getattr(_socket_socket, fun.__name__).__doc__ + return fun + + +class cosocket(object): + __doc__ = _socket_socket.__doc__ def __init__(self, family=_socket.AF_INET, type=_socket.SOCK_STREAM, proto=0, - _sock=None): - + fileno=None, _sock=None): + # This is the real socket object we will defer all calls to if _sock is None: - _sock = _socket_socket(family, type, proto) + if fileno is not None: + _sock = _socket_socket(family, type, proto, fileno) + else: + _sock = _socket_socket(family, type, proto) self.__socket = _sock self.__socket.setblocking(0) self.__timeout = _socket.getdefaulttimeout() @@ -121,25 +133,23 @@ def connect_ex(self, address): except _socket.error as error: return error.errno - def __poll(self, event): if not coselect.poll_list([(self, event)], self.__timeout): raise _socket.error(errno.ETIMEDOUT, 'Timeout waiting for socket') - def __retry(self, poll, action, args): + def __retry(self, event, action, args): while True: try: return action(*args) except _socket.error as error: if error.errno != errno.EAGAIN: raise - self.__poll(poll) - + self.__poll(event) @wrap def accept(self): sock, addr = self.__retry(coselect.POLLIN, self.__socket.accept, ()) - return (socket(_sock = sock), addr) + return (cosocket(_sock = sock), addr) @wrap def recv(self, *args): @@ -174,20 +184,37 @@ def sendall(self, data, *flags): @wrap def dup(self): - return socket(None, None, None, self.__socket.dup()) - - @wrap - def makefile(self, *args, **kws): - # At this point the actual socket '_socket.socket' is wrapped by either - # two layers: 'socket.socket' and this class. or a single layer: this - # class. In order to handle close() properly we must copy all wrappers, - # but not the underlying actual socket. - sock = getattr(self.__socket, '_sock', None) - if sock: # double wrapped - copy0 = _socket_socket(None, None, None, sock) - copy1 = socket(None, None, None, copy0) - else: # single wrapped - copy1 = socket(None, None, None, self.__socket) - return _socket._fileobject(copy1, *args, **kws) - - del wrap + return cosocket(_sock=self.__socket.dup()) + + if sys.version_info < (3,): + @wrap + def makefile(self, *args, **kws): + # At this point the actual socket '_socket.socket' is wrapped by + # either two layers: 'socket.socket' and this class. or a single + # layer: this class. In order to handle close() properly we must + # copy all wrappers, but not the underlying actual socket. + sock = getattr(self.__socket, '_sock', None) + if sock: # double wrapped + copy0 = _socket_socket(None, None, None, sock) + copy1 = cosocket(None, None, None, copy0) + else: # single wrapped + copy1 = cosocket(None, None, None, self.__socket) + return _socket._fileobject(copy1, *args, **kws) + else: + @property + def _io_refs(self): + return self.__socket._io_refs + + @_io_refs.setter + def _io_refs(self, value): + self.__socket._io_refs = value + + # Can use the original makefile just so long as we provide the _io_refs + # property above. + makefile = _socket_socket.makefile + + +del wrap + +# Make an alias to it +socket = cosocket diff --git a/cothread/cothread.py b/cothread/cothread.py index 1e7fd9e..5375453 100644 --- a/cothread/cothread.py +++ b/cothread/cothread.py @@ -74,15 +74,21 @@ import bisect import traceback import collections -import thread +import threading from . import _coroutine +from . import py23 if os.environ.get('COTHREAD_CHECK_STACK'): _coroutine.enable_check_stack(True) from . import coselect +if sys.version_info >= (3,): + import _thread +else: + import thread as _thread + __all__ = [ 'Spawn', # Spawn new task @@ -92,6 +98,7 @@ 'Yield', # Suspend task for immediate resumption 'Event', # Event for waiting and signalling + 'RLock', # Recursive lock 'Pulse', # Event for dynamic condition variables 'EventQueue', # Queue of objects with event handling 'ThreadedEventQueue', # Event queue designed to work with threads @@ -108,6 +115,8 @@ 'Timer', # One-shot cancellable timer 'Callback', # Simple asynchronous synchronisation + 'CallbackResult', # Asynchronous synchronisation with result + 'scheduler_thread_id', # For checking we're in cothread's thread ] @@ -248,7 +257,7 @@ def wakeup(self, reason): self.__task = None # Each queue needs to be cancelled if it's not the wakeup reason. - # This test also properly deals with _WAKEUP_INTERRUPT, which + # This test also properly deals with interrupt wakeup, which # requires both queues to be cancelled. if reason != _WAKEUP_NORMAL and self.__queue: self.__queue.cancel() @@ -269,7 +278,8 @@ def woken(self): # Task wakeup reasons _WAKEUP_NORMAL = 0 # Normal wakeup _WAKEUP_TIMEOUT = 1 # Wakeup on timeout -_WAKEUP_INTERRUPT = 2 # Special: transfer scheduler exception to main +# A third reason, transfering exception to another cothread, is encoded as a +# tuple. # Important system invariants: @@ -324,9 +334,9 @@ def __scheduler(cls, main_task): if task is main_task: del self.__ready_queue[index] break - # All task wakeup entry points will interpret this as a - # request to re-raise the exception. - _coroutine.switch(main_task, _WAKEUP_INTERRUPT) + # All task wakeup entry points will interpret this as a request + # to re-raise the exception. Pass through the exception info. + _coroutine.switch(main_task, sys.exc_info()) def __init__(self): # List of all tasks that are currently ready to be dispatched. @@ -430,9 +440,9 @@ def poll_scheduler(self, ready_list): result = _coroutine.switch(self.__coroutine, ready_list) self.__poll_callback = None - if result == _WAKEUP_INTERRUPT: + if isinstance(result, tuple): # This case arises if we are main and the scheduler just died. - raise + py23.raise_with_traceback(result) else: return result @@ -477,12 +487,12 @@ def wait_until(self, until, suspend_queue, wakeup): # returned to __select(). This last case expects a list of ready # descriptors to be returned, so we have to be compatible with this! result = _coroutine.switch(self.__coroutine, []) - if result == _WAKEUP_INTERRUPT: + if isinstance(result, tuple): # We get here if main is suspended and the scheduler decides # to die. Make sure our wakeup is cancelled, and then # re-raise the offending exception. wakeup.wakeup(result) - raise + py23.raise_with_traceback(result) else: return result == _WAKEUP_TIMEOUT @@ -509,7 +519,7 @@ def __Wakeup(self, queue, until): return _Wakeup(self.__wakeup_task, queue, self.__timer_queue) def __wakeup_task(self, task, reason): - if reason != _WAKEUP_INTERRUPT: + if not isinstance(reason, tuple): self.__ready_queue.append((task, reason)) def __wakeup_poll(self, poll_result): @@ -683,9 +693,10 @@ def __run(self, _): # See wait_until() for an explanation of this return value. return [] - def __nonzero__(self): + def __bool__(self): '''Tests whether the event is signalled.''' return bool(self.__result) + __nonzero__ = __bool__ def Wait(self, timeout = None): '''Waits until the task has completed. May raise an exception if the @@ -700,7 +711,7 @@ def Wait(self, timeout = None): try: # Re-raise the exception that actually killed the task here # where it can be received by whoever waits on the task. - raise result[0], result[1], result[2] + py23.raise_with_traceback(result) finally: # In this case result and self.__result contain a traceback. To # avoid circular references which will delay garbage collection, @@ -741,9 +752,10 @@ def __init__(self, auto_reset = True): self.__value = () self.__auto_reset = auto_reset - def __nonzero__(self): + def __bool__(self): '''Tests whether the event is signalled.''' return bool(self.__value) + __nonzero__ = __bool__ def Wait(self, timeout = None): '''The caller will block until the event becomes true, or until the @@ -870,6 +882,7 @@ def __iter__(self): def next(self): return self.Wait() + __next__ = next class ThreadedEventQueue(object): @@ -895,7 +908,7 @@ def Wait(self, timeout = None): '''Waits for a value to be written to the queue. This can safely be called from either a cothread or another thread: the appropriate form of cooperative or normal blocking will be selected automatically.''' - if thread.get_ident() == _scheduler_thread_id: + if _thread.get_ident() == scheduler_thread_id: # Normal cothread case, use cooperative wait poll = coselect.poll_list else: @@ -911,7 +924,7 @@ def Signal(self, value): '''Posts a value to the event queue. This can safely be called from a thread or a cothread.''' self.__values.append(value) - os.write(self.__signal, '-') + os.write(self.__signal, b'-') @@ -958,7 +971,48 @@ def __call__(self, action, *args): self.values.append((action, args)) if self.waiting: self.waiting = False - os.write(self.signal, '-') + os.write(self.signal, b'-') + + +def CallbackResult(action, *args, **kargs): + '''Perform action in the main cothread and return a result.''' + callback = kargs.pop('callback_queue', Callback) + timeout = kargs.pop('callback_timeout', None) + spawn = kargs.pop('callback_spawn', True) + + if scheduler_thread_id == _thread.get_ident(): + return action(*args, **kargs) + else: + event = threading.Event() + action_result = [False, None] + def do_action(): + try: + action_result[0] = True + action_result[1] = action(*args, **kargs) + except: + action_result[0] = False + action_result[1] = sys.exc_info() + event.set() + + # Hand the action over to the cothread carrying thread for action and + # wait for the result. + if spawn: + callback(Spawn, do_action) + else: + callback(do_action) + if not event.wait(timeout): + raise Timedout('Timed out waiting for callback result') + + # Return result or raise caught exception as appropriate. + ok, result = action_result + if ok: + return result + else: + py23.raise_with_traceback(result) + + # Note: raising entire stack backtrace context might be dangerous, need + # to think about this carefully, particularly if the corresponding stack + # has been swapped out... class Timer(object): @@ -1090,14 +1144,14 @@ def quit(signum, frame): _scheduler = _Scheduler.create() # We hang onto the thread ID for the cothread thread (at present there can # only be one) so that we can recognise when we're in another thread. -_scheduler_thread_id = thread.get_ident() +scheduler_thread_id = _thread.get_ident() # Thread validation: ensure cothreads aren't used across threads! def _validate_thread(): - assert _scheduler_thread_id == thread.get_ident(), \ - 'Cannot use cothread with multiple threads. Consider using ' \ - 'Callback or ThreadedEventQueue if necessary.' + assert scheduler_thread_id == _thread.get_ident(), \ + 'Cannot call into cothread from another thread. Consider using ' \ + 'Callback or CallbackResult.' # This is the asynchronous callback method. Callback = _Callback() @@ -1119,3 +1173,50 @@ def Yield(timeout = 0): waiting to be run.''' _validate_thread() _scheduler.do_yield(GetDeadline(timeout)) + + +class RLock(object): + """A reentrant lock.""" + + __slots__ = [ + '__event', # Underlying event object + '__owner', # The coroutine that has locked + '__count', # The number of times the owner has locked + ] + + def __init__(self): + self.__event = Event() + # Start off with the event set so acquire will not block + self.__event.Signal() + self.__owner = None + self.__count = 0 + + def acquire(self, timeout=None): + """Acquire the lock if necessary and increment the recursion level.""" + # Inspired by threading.RLock + me = _coroutine.get_current() + if self.__owner and _coroutine.is_equal(self.__owner, me): + # if we are the owner then just increment the count + self.__count += 1 + else: + # otherwise wait until it is unlocked + self.__event.Wait(timeout=timeout) + self.__owner = me + self.__count = 1 + + def release(self): + """Release a lock, decrementing the recursion level.""" + assert self.__owner and _coroutine.is_equal( + self.__owner, _coroutine.get_current()), \ + "cannot release un-acquired lock" + self.__count -= 1 + if self.__count == 0: + self.__owner = None + # Wakeup one cothread waiting on acquire() + self.__event.Signal() + + # Needed to make it a context manager + __enter__ = acquire + + def __exit__(self, t, v, tb): + self.release() diff --git a/cothread/dbr.py b/cothread/dbr.py index 1c152dc..51906de 100644 --- a/cothread/dbr.py +++ b/cothread/dbr.py @@ -31,11 +31,13 @@ header file db_access.h ''' +import sys import ctypes import numpy import datetime from . import cadef +from . import py23 __all__ = [ @@ -52,6 +54,7 @@ 'DBR_CHAR_STR', # Long strings as char arrays 'DBR_CHAR_UNICODE', # Long unicode strings as char arrays 'DBR_ENUM_STR', # Enums as strings, default otherwise + 'DBR_CHAR_BYTES', # Long byte strings as char arrays 'DBR_PUT_ACKT', # Configure global alarm acknowledgement 'DBR_PUT_ACKS', # Acknowledge global alarm @@ -161,11 +164,29 @@ class ca_str(str): def __pos__(self): return str(self) -class ca_unicode(unicode): - __doc__ = ca_doc_string - datetime = timestamp_to_datetime - def __pos__(self): - return unicode(self) + +# Overlapping handling for python 2 and python 3. We have three types with two +# different semantics: str, bytes, unicode. In python2 str is bytes, while in +# python3 str is unicode. We walk a delicate balancing act to get the right +# behaviour in both environments! +if sys.version_info < (3,): + ca_bytes = ca_str + class ca_unicode(bytes): + __doc__ = ca_doc_string + datetime = timestamp_to_datetime + def __pos__(self): + return unicode(self) + str_char_code = 'S' +else: + class ca_bytes(bytes): + __doc__ = ca_doc_string + datetime = timestamp_to_datetime + def __pos__(self): + return bytes(self) + ca_unicode = ca_str + str_char_code = 'U' + unicode = str + class ca_int(int): __doc__ = ca_doc_string @@ -220,7 +241,7 @@ def copy_attributes_ctrl(self, other): other.status = self.status other.severity = self.severity - other.units = ctypes.string_at(self.units) + other.units = py23.decode(ctypes.string_at(self.units)) other.upper_disp_limit = self.upper_disp_limit other.lower_disp_limit = self.lower_disp_limit other.upper_alarm_limit = self.upper_alarm_limit @@ -409,7 +430,9 @@ class dbr_ctrl_enum(ctypes.Structure): def copy_attributes(self, other): other.status = self.status other.severity = self.severity - other.enums = map(ctypes.string_at, self.raw_strs[:self.no_str]) + other.enums = [ + py23.decode(ctypes.string_at(s)) + for s in self.raw_strs[:self.no_str]] class dbr_ctrl_char(ctypes.Structure): dtype = numpy.uint8 @@ -516,6 +539,7 @@ def copy_attributes(self, other): # Special value for DBR_CHAR as str special processing. DBR_ENUM_STR = 996 +DBR_CHAR_BYTES = 997 DBR_CHAR_UNICODE = 998 DBR_CHAR_STR = 999 @@ -566,7 +590,7 @@ def copy_attributes(self, other): 'i': DBR_LONG, # intc = int32 'f': DBR_FLOAT, # single = float32 'd': DBR_DOUBLE, # float_ = float64 - 'S': DBR_STRING, # str_ + 'S': DBR_STRING, # bytes_ # The following type codes are weakly supported by pretending that # they're related types. @@ -606,9 +630,11 @@ def _datatype_to_dbr(datatype): # Rely on numpy for generic datatype recognition and conversion together # with filtering through our array of acceptable types. return NumpyCharCodeToDbr[numpy.dtype(datatype).char] - except: - raise InvalidDatatype( - 'Datatype "%s" not supported for channel access' % datatype) + except Exception as error: + py23.raise_from( + InvalidDatatype( + 'Datatype "%s" not supported for channel access' % datatype), + error) def _type_to_dbrcode(datatype, format): '''Converts a datatype and format request to a dbr value, or raises an @@ -623,7 +649,7 @@ def _type_to_dbrcode(datatype, format): - FORMAT_CTRL: retrieve limit and control data ''' if datatype not in BasicDbrTypes: - if datatype in [DBR_CHAR_STR, DBR_CHAR_UNICODE]: + if datatype in [DBR_CHAR_STR, DBR_CHAR_BYTES, DBR_CHAR_UNICODE]: datatype = DBR_CHAR # Retrieve this type using char array elif datatype in [DBR_STSACK_STRING, DBR_CLASS_NAME]: return datatype # format is meaningless in this case @@ -649,7 +675,7 @@ def _type_to_dbrcode(datatype, format): raise InvalidDatatype('Format not recognised') -# Helper functions for string arrays used in _convert_str_{str,unicode} below. +# Helper functions for string arrays used in _convert_str_{str,bytes} below. def _make_strings(raw_dbr, count): p_raw_value = ctypes.pointer(raw_dbr.raw_value[0]) return [ctypes.string_at(p_raw_value[n]) for n in range(count)] @@ -678,25 +704,38 @@ def _string_at(raw_value, count): # Conversion from char array to strings def _convert_char_str(raw_dbr, count): - return ca_str(_string_at(raw_dbr.raw_value, count)) + return ca_str(py23.decode(_string_at(raw_dbr.raw_value, count))) + +# Conversion from char array to bytes strings +def _convert_char_bytes(raw_dbr, count): + return ca_bytes(_string_at(raw_dbr.raw_value, count)) # Conversion from char array to unicode strings def _convert_char_unicode(raw_dbr, count): return ca_unicode(_string_at(raw_dbr.raw_value, count).decode('UTF-8')) + # Arrays of standard strings. def _convert_str_str(raw_dbr, count): - return ca_str(_make_strings(raw_dbr, count)[0]) + return ca_str(py23.decode(_make_strings(raw_dbr, count)[0])) def _convert_str_str_array(raw_dbr, count): + strings = [py23.decode(s) for s in _make_strings(raw_dbr, count)] + return _string_array(strings, count, str_char_code) + +# Arrays of bytes strings. +def _convert_str_bytes(raw_dbr, count): + return ca_bytes(_make_strings(raw_dbr, count)[0]) +def _convert_str_bytes_array(raw_dbr, count): return _string_array(_make_strings(raw_dbr, count), count, 'S') # Arrays of unicode strings. def _convert_str_unicode(raw_dbr, count): - return ca_unicode(_make_strings(raw_dbr, count)[0].decode('UTF-8')) + return ca_str(_make_strings(raw_dbr, count)[0].decode('UTF-8')) def _convert_str_unicode_array(raw_dbr, count): strings = [s.decode('UTF-8') for s in _make_strings(raw_dbr, count)] return _string_array(strings, count, 'U') + # For everything that isn't a string we either return a scalar or a ca_array def _convert_other(raw_dbr, count): # Single elements are always returned as scalars. @@ -745,13 +784,18 @@ def type_to_dbr(channel, datatype, format): if dtype is numpy.uint8 and datatype == DBR_CHAR_STR: # Conversion from char array to strings convert = _convert_char_str + elif dtype is numpy.uint8 and datatype == DBR_CHAR_BYTES: + # Conversion from char array to bytes strings + convert = _convert_char_bytes elif dtype is numpy.uint8 and datatype == DBR_CHAR_UNICODE: # Conversion from char array to unicode strings convert = _convert_char_unicode else: if dtype is str_dtype: # String arrays, either unicode or normal. - if isinstance(datatype, type) and issubclass(datatype, unicode): + if isinstance(datatype, type) and issubclass(datatype, bytes): + convert = (_convert_str_bytes, _convert_str_bytes_array) + elif isinstance(datatype, type) and issubclass(datatype, unicode): convert = (_convert_str_unicode, _convert_str_unicode_array) else: convert = (_convert_str_str, _convert_str_str_array) @@ -820,7 +864,7 @@ def value_to_dbr(channel, datatype, value): # If no datatype specified then use the target datatype. if datatype is None: - if isinstance(value, (str, unicode)): + if isinstance(value, (str, bytes, unicode)): # Give strings with no datatype special treatment, let the IOC do # the decoding. It's safer this way. datatype = DBR_STRING diff --git a/cothread/input_hook.py b/cothread/input_hook.py index e65e196..3de4fc0 100644 --- a/cothread/input_hook.py +++ b/cothread/input_hook.py @@ -90,7 +90,7 @@ def timeout(): # Set up a timer so that Qt polls cothread. All the timer needs to do # is to yield control to the coroutine system. - from PyQt4 import QtCore + from .qt import QtCore timer = QtCore.QTimer() timer.timeout.connect(timeout) timer.start(poll_interval * 1e3) @@ -114,7 +114,7 @@ def iqt(poll_interval = 0.05, run_exec = True, argv = None): '''Installs Qt event handling hook. The polling interval is in seconds.''' - from PyQt4 import QtCore, QtGui + from .qt import QtCore, QtWidgets global _qapp, _timer # Importing PyQt4 has an unexpected side effect: it removes the input hook! @@ -131,11 +131,11 @@ def iqt(poll_interval = 0.05, run_exec = True, argv = None): if _qapp is None: if argv is None: argv = sys.argv - _qapp = QtGui.QApplication(argv) + _qapp = QtWidgets.QApplication(argv) # Arrange to get a Quit event when the last window goes. This allows the # application to simply rest on WaitForQuit(). - _qapp.lastWindowClosed.connect(cothread.Quit) + _qapp.aboutToQuit.connect(cothread.Quit) # Create timer. Hang onto the timer to prevent it from vanishing. _timer = _timer_iqt(poll_interval) diff --git a/cothread/load_ca.py b/cothread/load_ca.py index 1184ad7..022aaac 100644 --- a/cothread/load_ca.py +++ b/cothread/load_ca.py @@ -51,6 +51,31 @@ lib_files = ['libca.so'] +# Mapping from host architecture to EPICS host architecture name can be done +# with a little careful guesswork. As EPICS architecture names are a little +# arbitrary this isn't guaranteed to work. +_epics_system_map = { + ('Linux', '32bit'): 'linux-x86', + ('Linux', '64bit'): 'linux-x86_64', + ('Darwin', '32bit'): 'darwin-x86', + ('Darwin', '64bit'): 'darwin-x86', + ('Windows', '32bit'): 'win32-x86', + ('Windows', '64bit'): 'windows-x64', # Not quite yet! +} + +def _get_arch(): + import os + try: + return os.environ['EPICS_HOST_ARCH'] + except KeyError: + import platform + system = platform.system() + bits = platform.architecture()[0] + return _epics_system_map[(system, bits)] + +epics_host_arch = _get_arch() + + def _libca_path(load_libca_path): # We look for libca in a variety of different places, searched in order: # @@ -84,19 +109,6 @@ def _libca_path(load_libca_path): # No local install, no local configuration, no override. Try for standard # environment variable configuration instead. epics_base = os.environ['EPICS_BASE'] - # Mapping from host architecture to EPICS host architecture name can be done - # with a little careful guesswork. As EPICS architecture names are a little - # arbitrary this isn't guaranteed to work. - system_map = { - ('Linux', '32bit'): 'linux-x86', - ('Linux', '64bit'): 'linux-x86_64', - ('Darwin', '32bit'): 'darwin-x86', - ('Darwin', '64bit'): 'darwin-x86', - ('Windows', '32bit'): 'win32-x86', - ('Windows', '64bit'): 'windows-x64', # Not quite yet! - } - bits = platform.architecture()[0] - epics_host_arch = system_map[(system, bits)] return os.path.join(epics_base, 'lib', epics_host_arch) diff --git a/cothread/pv.py b/cothread/pv.py index 07fd575..640c8ec 100644 --- a/cothread/pv.py +++ b/cothread/pv.py @@ -28,21 +28,32 @@ class PV(object): '''PV wrapper class. Wraps access to a single PV as a persistent object with simple access methods. Always contains the latest PV value. - WARNING! This API is a work in progress and will change in future releases + WARNING! This API is a work in progress and may change in future releases in incompatible ways.''' - def __init__(self, pv, on_update = None, timeout = 5, **kargs): + def __init__(self, pv, + on_update = None, initial_value = None, caput_wait = False, + initial_timeout = (), **kargs): + assert isinstance(pv, str), 'PV class only works for one PV at a time' self.name = pv - self.__event = cothread.Event() - self.__value = None + self.__value_event = cothread.Event() + self.__sync = cothread.Event(auto_reset = False) + self.__value = initial_value + self.caput_wait = caput_wait + self.datatype = kargs.get('datatype', None) + self.format = kargs.get('format', catools.FORMAT_RAW) + + self.__deadline_set = initial_timeout != () + assert initial_value is None or not self.__deadline_set, \ + 'Cannot specify initial value and initial timeout' + if self.__deadline_set: + self.__deadline = cothread.AbsTimeout(initial_timeout) + self.on_update = on_update self.__monitor = catools.camonitor( pv, _WeakMethod(self, '_on_update'), **kargs) - self.on_update = on_update - - self.__deadline = cothread.AbsTimeout(timeout) def __del__(self): self.close() @@ -52,32 +63,43 @@ def close(self): def _on_update(self, value): self.__value = value - self.__event.Signal(value) + self.__value_event.Signal(value) + self.__sync.Signal() if self.on_update: self.on_update(self) + def sync(self, timeout = ()): + '''Blocks until at least one update has been seen.''' + if timeout == (): + assert self.__deadline_set, 'Must specify sync timeout' + timeout = self.__deadline + catools.ca_timeout(self.__sync, timeout, self.name) + def get(self): '''Returns current value.''' - if self.__value is None: - return self.get_next(self.__deadline) - else: - return self.__value + if self.__value is None and self.__deadline_set: + catools.ca_timeout(self.__sync, self.__deadline, self.name) + return self.__value def get_next(self, timeout = None, reset = False): '''Returns current value or blocks until next update. Call .reset() first if more recent value required.''' if reset: self.reset() - return self.__event.Wait(timeout) + return catools.ca_timeout(self.__value_event, timeout, self.name) def reset(self): '''Ensures .get_next() will block until an update occurs.''' - self.__event.Reset() + self.__value_event.Reset() def caput(self, value, **kargs): + kargs.setdefault('wait', self.caput_wait) + kargs.setdefault('datatype', self.datatype) return catools.caput(self.name, value, **kargs) def caget(self, **kargs): + kargs.setdefault('datatype', self.datatype) + kargs.setdefault('format', self.format) return catools.caget(self.name, **kargs) value = property(get, caput) @@ -92,7 +114,8 @@ class PV_array(object): in incompatible ways.''' def __init__(self, pvs, - dtype = float, count = 1, on_update = None, **kargs): + dtype = float, count = 1, on_update = None, caput_wait = False, + **kargs): assert not isinstance(pvs, str), \ 'PV_array class only works for an array of PVs' @@ -101,6 +124,7 @@ def __init__(self, pvs, self.on_update = on_update self.dtype = dtype self.count = count + self.caput_wait = caput_wait if count == 1: self.shape = len(pvs) @@ -156,6 +180,7 @@ def sync(self, timeout = 5, throw = True): self._update_one(value, index) def caput(self, value, **kargs): + kargs.setdefault('wait', self.caput_wait) return catools.caput(self.names, value, **kargs) value = property(get, caput) diff --git a/cothread/py23.py b/cothread/py23.py new file mode 100644 index 0000000..4543873 --- /dev/null +++ b/cothread/py23.py @@ -0,0 +1,56 @@ +# Some simple python2/python3 compatibility tricks. +# +# Some of this is drastically reduced from six.py + +import sys +import ctypes + + +# Exception handling +if sys.version_info < (3,): + def raise_from(exception, source): + raise exception + + exec(''' +def raise_with_traceback(result): + raise result[0], result[1], result[2] +''') + +else: + exec(''' +def raise_from(exception, source): + try: + raise exception from source + finally: + exception = None +''') + + def raise_with_traceback(result): + raise result[1].with_traceback(result[2]) + + +# c_char_p conversion +if sys.version_info < (3,): + auto_encode = ctypes.c_char_p + def auto_decode(result, func, args): + return result + def decode(s): + return s + +else: + class auto_encode(ctypes.c_char_p): + @classmethod + def from_param(cls, value): + if value is None: + return value + else: + return value.encode('UTF-8') + + def auto_decode(result, func, args): + if result is None: + return result + else: + return result.decode('UTF-8') + + def decode(s): + return s.decode('UTF-8') diff --git a/cothread/qt.py b/cothread/qt.py new file mode 100644 index 0000000..574a0fa --- /dev/null +++ b/cothread/qt.py @@ -0,0 +1,33 @@ +import sys + + +qts = ['PyQt5', 'PyQt4'] ## ordered by preference + +# check if PyQt alredy imported +QT_LIB = None +for lib in qts: + if lib in sys.modules: + QT_LIB = lib + break + +# if not imported let's try to import any +if QT_LIB is None: + for lib in qts: + try: + __import__(lib) + QT_LIB = lib + break + except ImportError: + pass +if QT_LIB is None: + ImportError("PyQt not found") + +# now some PyQt is imported + +if QT_LIB == 'PyQt5': + from PyQt5 import QtCore, QtWidgets + + +elif QT_LIB == 'PyQt4': + from PyQt4 import QtCore, QtGui + QtWidgets = QtGui diff --git a/cothread/tools/pvtree.py b/cothread/tools/pvtree.py index 2e6c4a9..f303aff 100755 --- a/cothread/tools/pvtree.py +++ b/cothread/tools/pvtree.py @@ -1,4 +1,4 @@ -#!/bin/env dls-python +#!/usr/bin/env python # Simple tool for viewing the chain of PV dependencies. @@ -6,9 +6,9 @@ import sys import re +import os if __name__ == '__main__': - import os sys.path.append( os.path.join(os.path.dirname(__file__), '../..')) try: diff --git a/cothread/version.py b/cothread/version.py new file mode 100644 index 0000000..11a4806 --- /dev/null +++ b/cothread/version.py @@ -0,0 +1 @@ +__version__ = '2.15' diff --git a/debian/changelog b/debian/changelog index b658326..ffabaef 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,14 @@ -cothread (2.12-5) UNRELEASED; urgency=medium +cothread (2.15-1) UNRELEASED; urgency=medium + [ Michael Davidsaver ] * Added libepics dependency option for base 3.15.3 and 3.16.1 - -- Michael Davidsaver Fri, 27 Oct 2017 17:38:29 -0500 + [ Dylan Maxwell ] + * Add support for EPICS v3.15.6 + * New upstream version 2.15 + * Update package to use pybuild and support python3 + + -- Dylan Maxwell Fri, 07 Dec 2018 14:55:37 -0500 cothread (2.12-4) unstable; urgency=low diff --git a/debian/clean b/debian/clean new file mode 100644 index 0000000..abe6a03 --- /dev/null +++ b/debian/clean @@ -0,0 +1,3 @@ +# Fix problem with "local changes detected" +# See details: https://wiki.debian.org/Python/FAQ +*.egg-info/* diff --git a/debian/compat b/debian/compat index 7f8f011..ec63514 100644 --- a/debian/compat +++ b/debian/compat @@ -1 +1 @@ -7 +9 diff --git a/debian/control b/debian/control index a0bca17..c370c74 100644 --- a/debian/control +++ b/debian/control @@ -7,8 +7,13 @@ Build-Depends: debhelper (>= 7), python-all-dev, python-all-dbg, dh-python, python-numpy, python-sphinx, - libepics3.14.11 | libepics3.14.12 | libepics3.14.12.3 | libepics3.15.3 | libepics3.16.1, + python-docutils, + python3-all-dev, + python3-all-dbg, + python3-setuptools, + libepics3.14.11 | libepics3.14.12 | libepics3.14.12.3 | libepics3.15.3 | libepics3.15.6 | libepics3.16.1, XS-Python-Version: >= 2.7 +XS-Python3-Version: >= 3.4 Standards-Version: 3.8.0 Homepage: http://controls.diamond.ac.uk/downloads/python/cothread/ @@ -16,10 +21,28 @@ Package: python-cothread Architecture: any Depends: ${shlibs:Depends}, ${python:Depends}, python-setuptools, python-numpy, - libepics3.14.11 | libepics3.14.12 | libepics3.14.12.3 | libepics3.15.3 | libepics3.16.1, + libepics3.14.11 | libepics3.14.12 | libepics3.14.12.3 | libepics3.15.3 | libepics3.15.6 | libepics3.16.1, Conflicts: python-cothread-doc (<< 1.15) -XB-Python-Version: ${python:Versions} Suggests: python-cothread-doc -Description: A co-routine implimentation w/ EPICS CA +Description: A co-routine implimentation w/ EPICS CA (Python 2) + An implimentation of co-routines in Python with + a ctypes wrapper for the EPICS libca client library + +Package: python3-cothread +Architecture: any +Depends: ${shlibs:Depends}, ${python3:Depends}, + python-setuptools, python-numpy, + libepics3.14.11 | libepics3.14.12 | libepics3.14.12.3 | libepics3.15.3 | libepics3.15.6 | libepics3.16.1, +Conflicts: python-cothread-doc (<< 1.15) +Suggests: python-cothread-doc +Description: A co-routine implimentation w/ EPICS CA (Python 3) + An implimentation of co-routines in Python with + a ctypes wrapper for the EPICS libca client library + +Package: python-cothread-doc +Architecture: all +Section: doc +Depends: ${sphinxdoc:Depends}, ${misc:Depends}, +Description: A co-routine implimentation w/ EPICS CA (Documentation) An implimentation of co-routines in Python with a ctypes wrapper for the EPICS libca client library diff --git a/debian/patches/0001-fix-script-interpreter.patch b/debian/patches/0001-fix-script-interpreter.patch index 00ca7b0..804f3f5 100644 --- a/debian/patches/0001-fix-script-interpreter.patch +++ b/debian/patches/0001-fix-script-interpreter.patch @@ -1,239 +1,238 @@ -From: Michael Davidsaver -Date: Sat, 4 Oct 2014 12:12:46 -0400 +From: Dylan Maxwell +Date: Fri, 30 Nov 2018 11:33:35 -0500 Subject: fix script interpreter -use distro default /usr/bin/python --- - context/tests/coco.py | 2 +- - context/tests/leak.py | 2 +- - cothread/tools/pvtree.py | 2 +- - examples/camonitor.py | 2 +- - examples/caput.py | 2 +- - examples/qt_monitor.py | 2 +- - examples/scope_epics.py | 2 +- - examples/simple.py | 2 +- - setup.py | 2 +- - tests/caget_failure.py | 2 +- - tests/caget_structure.py | 2 +- - tests/camonitor.big.py | 2 +- - tests/camonitor_test.py | 2 +- - tests/interactive.py | 2 +- - tests/leaktest.py | 2 +- - tests/load.py | 2 +- - tests/plottest.py | 2 +- - tests/test-modal.py | 2 +- - tests/test-select.py | 2 +- - tests/testthreads.py | 2 +- - tests/timing-test.py | 2 +- + context/tests/coco.py | 2 +- + context/tests/leak.py | 2 +- + cothread/tools/pvtree.py | 2 +- + examples/camonitor.py | 2 +- + examples/caput.py | 2 +- + examples/qt_monitor.py | 2 +- + examples/scope_epics.py | 2 +- + examples/simple.py | 2 +- + old_tests/caget_failure.py | 2 +- + old_tests/caget_structure.py | 2 +- + old_tests/camonitor.big.py | 2 +- + old_tests/camonitor_test.py | 2 +- + old_tests/interactive.py | 2 +- + old_tests/leaktest.py | 2 +- + old_tests/load.py | 2 +- + old_tests/plottest.py | 2 +- + old_tests/test-modal.py | 2 +- + old_tests/test-select.py | 2 +- + old_tests/testthreads.py | 2 +- + old_tests/timing-test.py | 2 +- + setup.py | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/context/tests/coco.py b/context/tests/coco.py -index 4464d9b..6661a07 100755 +index d86bf08..6661a07 100755 --- a/context/tests/coco.py +++ b/context/tests/coco.py @@ -1,4 +1,4 @@ --#!/dls_sw/tools/python2.4-debug/bin/python2.4 +-#!/usr/bin/env python +#!/usr/bin/python from __future__ import print_function diff --git a/context/tests/leak.py b/context/tests/leak.py -index 8c886dd..b852863 100755 +index ac0c735..b852863 100755 --- a/context/tests/leak.py +++ b/context/tests/leak.py @@ -1,4 +1,4 @@ --#!/dls_sw/tools/python2.4-debug/bin/python2.4 +-#!/usr/bin/env python +#!/usr/bin/python from __future__ import print_function diff --git a/cothread/tools/pvtree.py b/cothread/tools/pvtree.py -index 2e6c4a9..209aba8 100755 +index f303aff..4672778 100755 --- a/cothread/tools/pvtree.py +++ b/cothread/tools/pvtree.py @@ -1,4 +1,4 @@ --#!/bin/env dls-python +-#!/usr/bin/env python +#!/usr/bin/python # Simple tool for viewing the chain of PV dependencies. diff --git a/examples/camonitor.py b/examples/camonitor.py -index 61ef6a7..65fff10 100755 +index a5354e7..65fff10 100755 --- a/examples/camonitor.py +++ b/examples/camonitor.py @@ -1,4 +1,4 @@ --#!/usr/bin/env dls-python2.6 +-#!/usr/bin/env python +#!/usr/bin/python # Simple example of camonitor tool catools library from __future__ import print_function diff --git a/examples/caput.py b/examples/caput.py -index fc6ce08..f4a0849 100755 +index cb971ca..f4a0849 100755 --- a/examples/caput.py +++ b/examples/caput.py @@ -1,4 +1,4 @@ --#!/usr/bin/env dls-python2.6 +-#!/usr/bin/env python +#!/usr/bin/python # Simple example of caget tool using cothread. from __future__ import print_function diff --git a/examples/qt_monitor.py b/examples/qt_monitor.py -index 25d26d8..eafc357 100755 +index 6845599..eafc357 100755 --- a/examples/qt_monitor.py +++ b/examples/qt_monitor.py @@ -1,4 +1,4 @@ --#!/usr/bin/env dls-python2.6 +-#!/usr/bin/env python +#!/usr/bin/python '''minimal Qt example''' diff --git a/examples/scope_epics.py b/examples/scope_epics.py -index fb0e65c..df62760 100755 +index 742a877..d36d0da 100755 --- a/examples/scope_epics.py +++ b/examples/scope_epics.py @@ -1,4 +1,4 @@ --#!/usr/bin/env dls-python2.6 +-#!/usr/bin/env python +#!/usr/bin/python '''Form Example with Monitor''' diff --git a/examples/simple.py b/examples/simple.py -index 3116629..6902758 100755 +index 85bd23f..6902758 100755 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,4 +1,4 @@ --#!/bin/env dls-python2.6 +-#!/usr/bin/env python +#!/usr/bin/python '''Channel Access Example''' -diff --git a/setup.py b/setup.py -index ed9e807..ec13e77 100755 ---- a/setup.py -+++ b/setup.py +diff --git a/old_tests/caget_failure.py b/old_tests/caget_failure.py +index bb0d373..4854e64 100755 +--- a/old_tests/caget_failure.py ++++ b/old_tests/caget_failure.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python -+#!/usr/bin/python - - import glob - import os -diff --git a/tests/caget_failure.py b/tests/caget_failure.py -index 5a2b486..08bea8f 100755 ---- a/tests/caget_failure.py -+++ b/tests/caget_failure.py -@@ -1,4 +1,4 @@ --#!/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 '''Channel Access Get Structure''' -diff --git a/tests/caget_structure.py b/tests/caget_structure.py -index 1d786a7..fb6ca1b 100755 ---- a/tests/caget_structure.py -+++ b/tests/caget_structure.py +diff --git a/old_tests/caget_structure.py b/old_tests/caget_structure.py +index bc39c7f..2821c16 100755 +--- a/old_tests/caget_structure.py ++++ b/old_tests/caget_structure.py @@ -1,4 +1,4 @@ --#!/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 '''Channel Access Get Structure''' -diff --git a/tests/camonitor.big.py b/tests/camonitor.big.py -index 7615dcb..1f90d6b 100755 ---- a/tests/camonitor.big.py -+++ b/tests/camonitor.big.py +diff --git a/old_tests/camonitor.big.py b/old_tests/camonitor.big.py +index c5ff6fa..58be578 100755 +--- a/old_tests/camonitor.big.py ++++ b/old_tests/camonitor.big.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 # Simple example of camonitor tool using greenlets etcetera. - from __future__ import print_function -diff --git a/tests/camonitor_test.py b/tests/camonitor_test.py -index 6ff3b61..56d11c0 100755 ---- a/tests/camonitor_test.py -+++ b/tests/camonitor_test.py + import sys +diff --git a/old_tests/camonitor_test.py b/old_tests/camonitor_test.py +index 90c97b1..077b66a 100755 +--- a/old_tests/camonitor_test.py ++++ b/old_tests/camonitor_test.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 '''camonitor minimal example''' -diff --git a/tests/interactive.py b/tests/interactive.py -index 5e7f0fc..13a3bea 100755 ---- a/tests/interactive.py -+++ b/tests/interactive.py +diff --git a/old_tests/interactive.py b/old_tests/interactive.py +index 764bd59..bee6272 100755 +--- a/old_tests/interactive.py ++++ b/old_tests/interactive.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 - from __future__ import print_function + import os -diff --git a/tests/leaktest.py b/tests/leaktest.py -index 392afb8..ae88303 100755 ---- a/tests/leaktest.py -+++ b/tests/leaktest.py +diff --git a/old_tests/leaktest.py b/old_tests/leaktest.py +index 1ab1084..b05edc5 100755 +--- a/old_tests/leaktest.py ++++ b/old_tests/leaktest.py @@ -1,4 +1,4 @@ --#!/dls_sw/tools/python2.4-debug/bin/python2.4 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 '''Tests for memory leaks.''' -diff --git a/tests/load.py b/tests/load.py -index eee4a02..7b53cb4 100755 ---- a/tests/load.py -+++ b/tests/load.py +diff --git a/old_tests/load.py b/old_tests/load.py +index 7f24b57..5618a01 100755 +--- a/old_tests/load.py ++++ b/old_tests/load.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 - from __future__ import print_function + import require -diff --git a/tests/plottest.py b/tests/plottest.py -index 8da9e93..a9e8797 100755 ---- a/tests/plottest.py -+++ b/tests/plottest.py +diff --git a/old_tests/plottest.py b/old_tests/plottest.py +index 4a17643..8bdfde4 100755 +--- a/old_tests/plottest.py ++++ b/old_tests/plottest.py @@ -1,4 +1,4 @@ --#!/usr/bin/env dls-python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 - from __future__ import print_function + import sys -diff --git a/tests/test-modal.py b/tests/test-modal.py -index c710537..f3acb44 100755 ---- a/tests/test-modal.py -+++ b/tests/test-modal.py +diff --git a/old_tests/test-modal.py b/old_tests/test-modal.py +index d3c6ef1..a6236f0 100755 +--- a/old_tests/test-modal.py ++++ b/old_tests/test-modal.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 - from __future__ import print_function - -diff --git a/tests/test-select.py b/tests/test-select.py -index 8dd5ae3..fc1cf98 100755 ---- a/tests/test-select.py -+++ b/tests/test-select.py + import require + import cothread +diff --git a/old_tests/test-select.py b/old_tests/test-select.py +index a67a1a6..a7716b6 100755 +--- a/old_tests/test-select.py ++++ b/old_tests/test-select.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 # Testing coselect -diff --git a/tests/testthreads.py b/tests/testthreads.py -index 97e3218..dd80197 100755 ---- a/tests/testthreads.py -+++ b/tests/testthreads.py +diff --git a/old_tests/testthreads.py b/old_tests/testthreads.py +index 50823e9..d299e8c 100755 +--- a/old_tests/testthreads.py ++++ b/old_tests/testthreads.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 -+#!/usr/bin/python +-#!/usr/bin/env python3 ++#!/usr/bin/python3 - from __future__ import print_function + import require + from cothread.cothread import * +diff --git a/old_tests/timing-test.py b/old_tests/timing-test.py +index 15ecac7..d46ae34 100755 +--- a/old_tests/timing-test.py ++++ b/old_tests/timing-test.py +@@ -1,4 +1,4 @@ +-#!/usr/bin/env python3 ++#!/usr/bin/python3 -diff --git a/tests/timing-test.py b/tests/timing-test.py -index 5a2f969..94df705 100755 ---- a/tests/timing-test.py -+++ b/tests/timing-test.py + import greenlet + import time +diff --git a/setup.py b/setup.py +index 097e37a..929f128 100755 +--- a/setup.py ++++ b/setup.py @@ -1,4 +1,4 @@ --#!/usr/bin/env python2.6 +-#!/usr/bin/env python +#!/usr/bin/python - from __future__ import print_function - + import glob + import os diff --git a/debian/patches/0002-remove-unused-Makefiles.patch b/debian/patches/0002-remove-unused-Makefiles.patch index c787441..8de3d39 100644 --- a/debian/patches/0002-remove-unused-Makefiles.patch +++ b/debian/patches/0002-remove-unused-Makefiles.patch @@ -3,18 +3,18 @@ Date: Sat, 4 Oct 2014 12:13:32 -0400 Subject: remove unused Makefiles --- - Makefile | 48 ------------------------------------------------ + Makefile | 62 --------------------------------------------------------- Makefile.config | 10 ---------- - 2 files changed, 58 deletions(-) + 2 files changed, 72 deletions(-) delete mode 100644 Makefile delete mode 100644 Makefile.config diff --git a/Makefile b/Makefile deleted file mode 100644 -index d6331df..0000000 +index 4a15af6..0000000 --- a/Makefile +++ /dev/null -@@ -1,48 +0,0 @@ +@@ -1,62 +0,0 @@ -TOP = . - -# This includes Makefile.private which is written by the make system, before @@ -48,9 +48,20 @@ index d6331df..0000000 - --install-dir=$(INSTALL_DIR) \ - --script-dir=$(SCRIPT_DIR) dist/*.egg - +-# publish +-publish: default +- $(PYTHON) setup.py sdist upload -r pypi +- +-# publish to test pypi +-testpublish: default +- $(PYTHON) setup.py sdist upload -r pypitest +- -docs: cothread/_coroutine.so - sphinx-build -b html docs docs/html - +-test: +- $(PYTHON) setup.py test +- -clean_docs: - rm -rf docs/html - @@ -63,6 +74,9 @@ index d6331df..0000000 - -cothread/_coroutine.so: $(wildcard context/*.c context/*.h) - $(PYTHON) setup.py build_ext -i +- +-build_ext: cothread/_coroutine.so +-.PHONY: build_ext diff --git a/Makefile.config b/Makefile.config deleted file mode 100644 index d8bde6b..0000000 diff --git a/debian/patches/0003-simpler-debian-specific-load_ca.py.patch b/debian/patches/0003-simpler-debian-specific-load_ca.py.patch index 73a81f7..b04a0b0 100644 --- a/debian/patches/0003-simpler-debian-specific-load_ca.py.patch +++ b/debian/patches/0003-simpler-debian-specific-load_ca.py.patch @@ -1,14 +1,14 @@ -From: Michael Davidsaver -Date: Sat, 4 Oct 2014 12:15:42 -0400 +From: Dylan Maxwell +Date: Fri, 30 Nov 2018 13:41:12 -0500 Subject: simpler debian specific load_ca.py --- - cothread/cadef.py | 3 +- - cothread/load_ca.py | 109 ++++++++++++++++------------------------------------ - 2 files changed, 34 insertions(+), 78 deletions(-) + cothread/cadef.py | 3 +- + cothread/load_ca.py | 99 +++++++++++++++++++---------------------------------- + 2 files changed, 36 insertions(+), 66 deletions(-) diff --git a/cothread/cadef.py b/cothread/cadef.py -index 6651b00..106ce84 100644 +index 8d98fa9..c03bda6 100644 --- a/cothread/cadef.py +++ b/cothread/cadef.py @@ -33,7 +33,7 @@ See http://www.aps.anl.gov/epics/base/R3-14/11-docs/CAref.html for detailed @@ -20,19 +20,19 @@ index 6651b00..106ce84 100644 ''' __all__ = [ -@@ -398,7 +398,6 @@ ca_host_name = libca.ca_host_name - ca_host_name.argtypes = [ctypes.c_void_p] +@@ -402,7 +402,6 @@ ca_host_name.argtypes = [ctypes.c_void_p] ca_host_name.restype = ctypes.c_char_p + ca_host_name.errcheck = py23.auto_decode - # read = ca_read_access(channel_id) # write = ca_write_access(channel_id) # diff --git a/cothread/load_ca.py b/cothread/load_ca.py -index 1184ad7..d5f831b 100644 +index 022aaac..319d63a 100644 --- a/cothread/load_ca.py +++ b/cothread/load_ca.py -@@ -32,94 +32,51 @@ +@@ -32,6 +32,10 @@ # This file can also be run as a standalone script to discover the path to # libca. @@ -43,11 +43,8 @@ index 1184ad7..d5f831b 100644 from __future__ import print_function import ctypes - import platform - import os - -- --# Figure out the libraries that need to be loaded and the loading method. +@@ -42,14 +46,9 @@ import os + # Figure out the libraries that need to be loaded and the loading method. load_library = ctypes.cdll.LoadLibrary system = platform.system() -if system == 'Windows': @@ -58,9 +55,16 @@ index 1184ad7..d5f831b 100644 -else: - lib_files = ['libca.so'] -+if system!='Linux': ++if system != 'Linux': + raise OSError('This version of cothread has been patched in a way which only works on Linux') + # Mapping from host architecture to EPICS host architecture name can be done + # with a little careful guesswork. As EPICS architecture names are a little +@@ -75,63 +74,35 @@ def _get_arch(): + + epics_host_arch = _get_arch() + +- -def _libca_path(load_libca_path): - # We look for libca in a variety of different places, searched in order: - # @@ -69,11 +73,15 @@ index 1184ad7..d5f831b 100644 - # 2 If the libca_path module is present we accept the value it defines. - # 3. Check for local copies of the libca file(s). - # 4. Finally check for EPICS_BASE and compute appropriate architecture - +- - # First allow a forced override - libca_path = os.environ.get('CATOOLS_LIBCA_PATH') - if libca_path: - return libca_path +- +- # Next import from configuration file if present, unless this has been +- # disabled. +- if load_libca_path: +# Known to be ABI compatible SO names for libca +# Extend the list of directories search in the usual way (eg. LD_LIBRARY_PATH) +libnames = [ @@ -81,15 +89,13 @@ index 1184ad7..d5f831b 100644 + 'libca.so.3.14.12', + 'libca.so.3.14.12.3', + 'libca.so.3.15.3', ++ 'libca.so.3.15.6', + 'libca.so.3.16.1', +] +# Allow user to provide additional names (eg "libca.so.3.15:libca.so.3.15.1") +# These are checked first. -+libnames = filter(len, os.environ.get('LIBCA_NAMES',"").split(':') ) + libnames - -- # Next import from configuration file if present, unless this has been -- # disabled. -- if load_libca_path: ++libnames = list(filter(len, os.environ.get('LIBCA_NAMES',"").split(':'))) + libnames ++ +def findca(): + for name in libnames: try: @@ -108,19 +114,6 @@ index 1184ad7..d5f831b 100644 - # No local install, no local configuration, no override. Try for standard - # environment variable configuration instead. - epics_base = os.environ['EPICS_BASE'] -- # Mapping from host architecture to EPICS host architecture name can be done -- # with a little careful guesswork. As EPICS architecture names are a little -- # arbitrary this isn't guaranteed to work. -- system_map = { -- ('Linux', '32bit'): 'linux-x86', -- ('Linux', '64bit'): 'linux-x86_64', -- ('Darwin', '32bit'): 'darwin-x86', -- ('Darwin', '64bit'): 'darwin-x86', -- ('Windows', '32bit'): 'win32-x86', -- ('Windows', '64bit'): 'windows-x64', # Not quite yet! -- } -- bits = platform.architecture()[0] -- epics_host_arch = system_map[(system, bits)] - return os.path.join(epics_base, 'lib', epics_host_arch) - + lib = load_library(name) @@ -133,15 +126,16 @@ index 1184ad7..d5f831b 100644 +in your environment. If your libca has a different (or no) SONAME then +Set LIBCA_NAMES to a colon seperated list of SONAMEs. +"""%(', '.join(libnames))) ++ ++libca, libca_name = findca() --if __name__ == '__main__': + if __name__ == '__main__': - # If run standalone we are a helper script. Write out the relevant - # definitions for the use of our caller. - libca_path = _libca_path(False) - print('CATOOLS_LIBCA_PATH=\'%s\'' % libca_path) - print('LIB_FILES=\'%s\'' % ' '.join(lib_files)) -+libca, libca_name = findca() - +- -else: - # Load the library (or libraries). - try: @@ -158,5 +152,4 @@ index 1184ad7..d5f831b 100644 - else: - for lib in lib_files: - libca = load_library(os.path.join(libca_path, lib)) -+if __name__=='__main__': + print("Found libca as: '%s'"%libca_name) diff --git a/debian/patches/0004-adjust-setup.py.patch b/debian/patches/0004-adjust-setup.py.patch index 6513796..d9a888b 100644 --- a/debian/patches/0004-adjust-setup.py.patch +++ b/debian/patches/0004-adjust-setup.py.patch @@ -1,19 +1,16 @@ -From: Michael Davidsaver -Date: Sat, 4 Oct 2014 12:17:30 -0400 +From: Dylan Maxwell +Date: Fri, 30 Nov 2018 11:40:00 -0500 Subject: adjust setup.py -hardcode version -remove -Werror -don't install pvtree to /usr/bin --- - setup.py | 9 +-------- - 1 file changed, 1 insertion(+), 8 deletions(-) + setup.py | 8 +++----- + 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py -index ec13e77..f2781df 100755 +index 929f128..f147918 100755 --- a/setup.py +++ b/setup.py -@@ -9,9 +9,6 @@ try: +@@ -10,9 +10,6 @@ try: from setuptools import setup, Extension setup_args = dict( @@ -23,26 +20,31 @@ index ec13e77..f2781df 100755 install_requires = ['numpy'], zip_safe = False) -@@ -20,13 +17,9 @@ except ImportError: - setup_args = {} - - --# these lines allow the version to be specified in Makefile.RELEASE --version = os.environ.get('MODULEVER', 'unknown') -- - # Extension module providing core coroutine functionality. Very similar in - # spirit to greenlet. - extra_compile_args = [ -- '-Werror', - '-Wall', - '-Wextra', +@@ -47,8 +44,10 @@ extra_compile_args = [ '-Wno-unused-parameter', -@@ -53,7 +46,7 @@ if platform.system() == 'Windows': + '-Wno-missing-field-initializers', + '-Wundef', ++ '-Wshadow', + '-Wcast-align', + '-Wwrite-strings', ++ '-Wredundant-decls', + '-Wmissing-prototypes', + '-Wmissing-declarations', + '-Wstrict-prototypes'] +@@ -66,7 +65,7 @@ if platform.system() == 'Windows': setup( name = 'cothread', -- version = version, -+ version = '2.12', +- version = get_version(), ++ version = '2.15', description = 'Cooperative threading based utilities', + long_description = open('README.rst').read(), author = 'Michael Abbott', - author_email = 'Michael.Abbott@diamond.ac.uk', +@@ -75,7 +74,6 @@ setup( + license = 'GPL2', + packages = ['cothread', 'cothread.tools'], + ext_modules = ext_modules, +- test_suite="tests", + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', diff --git a/debian/python-cothread.install b/debian/python-cothread-doc.install similarity index 100% rename from debian/python-cothread.install rename to debian/python-cothread-doc.install diff --git a/debian/rules b/debian/rules index febac12..f84db8a 100755 --- a/debian/rules +++ b/debian/rules @@ -2,13 +2,14 @@ export DH_VERBOSE=1 -PYVERS=$(shell pyversions -vr debian/control) +export PYBUILD_NAME=cothread %: - dh -Spython_distutils $@ -binary: binary-arch binary-indep -binary-arch binary-indep: install -install: build + dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild + + +override_dh_auto_test: + # Test named 'test_longout' is fails override_dh_auto_build: dh_auto_build diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..e864b8a --- /dev/null +++ b/debian/source/options @@ -0,0 +1,3 @@ +# Fix problem with "local changes detected" +# See details: https://wiki.debian.org/Python/FAQ +extend-diff-ignore="^[^/]+\.egg-info/" diff --git a/docrequirements.txt b/docrequirements.txt new file mode 100644 index 0000000..24ce15a --- /dev/null +++ b/docrequirements.txt @@ -0,0 +1 @@ +numpy diff --git a/docs/catools.rst b/docs/catools.rst index 056153c..d09a8ad 100644 --- a/docs/catools.rst +++ b/docs/catools.rst @@ -425,6 +425,13 @@ Functions ``.ok==False`` is returned. +.. function:: cainfo(pvs, timeout=5, throw=True) + + This is an alias for :func:`connect` with `cainfo` and `wait` set to + ``True``. Returns a :class:`ca_info` structure containing information about + the connected PV or a list of structures, as appropriate. + + .. _Values: Working with Values @@ -587,12 +594,13 @@ used to control the type of the data returned: 4. Any :class:`numpy.dtype` compatible with any of the above values. - 5. One of the special values :const:`DBR_CHAR_STR` or - :const:`DBR_CHAR_UNICODE`. This is used to request a char array which - is then converted to a Python string or Unicode string on receipt. It - is not sensible to specify `count` with this option. The option - :const:`DBR_CHAR_UNICODE` is meaningless and not supported - for :func:`caput`. + 5. One of the special values :const:`DBR_CHAR_STR`, + :const:`DBR_CHAR_UNICODE`, or :const:`DBR_CHAR_BYTES`. This is used to + request a char array which is then converted to a Python :class:`str` + :class:`unicode` or :class:`bytes` string on receipt. It is not + sensible to specify `count` with this option. The options + :const:`DBR_CHAR_BYTES` and :const:`DBR_CHAR_UNICODE` are meaningless + and not supported for :func:`caput`. Note that if the PV name ends in ``$`` and `datatype` is not specified then :const:`DBR_CHAR_STR` will be used. @@ -894,13 +902,32 @@ deleted. change in future releases. -.. class:: PV(pv, on_update=None, timeout=5, **kargs) +.. class:: PV(pv, on_update=None, initial_value=None, caput_wait=False, \ + [initial_timeout], **kargs) Creates a wrapper to monitor *pv*. If an *on_update* function is passed it will be called with the class instance as argument after each update to the - instance. The *timeout* is used the first time the class is interrogated to - check whether a connection has been established. The *kargs* are passed - through to the called :func:`camonitor`. + instance. The *kargs* are passed through to the called :func:`camonitor`. + The flag *caput_wait* can be set to change the default behaviour of + :meth:`caput`. + + The behaviour of the first call to :meth:`get` is affected by two arguments, + *initial_value* and *initial_timeout*, at most one of which can be + specified. If *initial_timeout* is specified then the first call to + :meth:`get` will block until this timeout expires or a valid PV value is + available. Otherwise *initial_value* can be set to specify a value to + return until the PV has updated. + + .. note:: + + This is an incompatible change from cothread versions 2.11 and 2.12. In + these versions the *initial_timeout* argument is named *timeout*, + defaults to 5, and cannot be unset. + + Note that blocking on a PV object for the initial update cannot be safely + done from within a camonitor callback, as in this case the blocking + operation is waiting for a camonitor callback to occur, and only one + camonitor callback is processed at a time. .. method:: close() @@ -908,6 +935,13 @@ deleted. Note that it is sufficient to drop all references to the class, it will then automatically call :meth:`close`. + .. method:: sync([timeout]) + + This call will block until the :class:`PV` object has seen at least one + update. If *initial_timeout* was specified in the constructor then its + associated deadline can be used as a default timeout, otherwise a + *timeout* must be specified. + .. method:: get() Returns the current value associated with the PV. This will be the most @@ -941,7 +975,9 @@ deleted. .. method:: caput(value, ** kargs) Directly calls :func:`caput` on the underlying PV with the given - arguments. + arguments. If *caput_wait* was set in the original :class:`PV` + constructor then by default :func:`caput` is called with ``wait=True``, + otherwise :func:`caput` is non blocking. .. attribute:: name @@ -955,14 +991,17 @@ deleted. new_value)``. -.. class:: PV_array(pvs, dtype=float, count=1, on_update=None, **kargs) +.. class:: PV_array(pvs, dtype=float, count=1, on_update=None, \ + caput_wait=False, **kargs) Uses *pvs* to create an aggregate array containing the value of all specified PVs aggregated into a single :mod:`numpy` array. The type of all the elements is specified by *dtype* and the number of points contributed by each PV is given by *count*. If *count* is 1 the generated array is one dimensional of shape ``(len(pvs),)``, otherwise the shape is - ``(len(pvs),count)``. + ``(len(pvs),count)``. The flag *caput_wait* can be set to change the + default behaviour of :meth:`caput`. + At the same time arrays of length ``len(pvs)`` are created for the connection status, timestamp and severity of each PV. @@ -990,7 +1029,11 @@ deleted. .. method:: caput(value, ** args) - Directly calls :func:`caput` on the stored list of PVs. + Directly calls :func:`caput` on the stored list of PVs. If *caput_wait* + was set in the original :class:`PV` constructor then by default + :func:`caput` is called with ``wait=True``, otherwise :func:`caput` is + non blocking. + .. method:: sync(timeout=5, throw=False) diff --git a/docs/conf.py b/docs/conf.py index 66fe0e3..9de59f2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,8 +18,11 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) -from pkg_resources import require -require('numpy') +try: + from pkg_resources import require + require('numpy') +except: + pass sys.path.append(os.path.abspath('..')) # -- General configuration ----------------------------------------------------- @@ -49,7 +52,7 @@ # General information about the project. project = u'Cothread' -copyright = u'2011, Michael Abbott' +copyright = u'2007-2015, Michael Abbott, Diamond Light Source Ltd' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/cothread.rst b/docs/cothread.rst index 06b4f86..ec9d587 100644 --- a/docs/cothread.rst +++ b/docs/cothread.rst @@ -142,10 +142,11 @@ not to suspend): This is always a suspension point. :func:`catools.caput` - This routine will normally cause the caller to suspend. To avoid - suspension, only put to one PV, use ``wait=False``, and ensure that the - channel is already connected -- this will be the case if it has already - been successfully used in any :mod:`catools` method. + This routine may cause the caller to suspend. To avoid suspension, put to + only one PV, use ``wait=False`` (the default), and ensure that the channel + is already connected -- this will be the case if it has already been + successfully used in any :mod:`catools` method. To ensure suspension use + ``wait=True``. The :mod:`cothread.cosocket` module makes most socket operations into suspension points when the corresponding socket operation is not yet ready. @@ -352,6 +353,59 @@ and :class:`EventQueue` objects. A :class:`Pulse` holds no values, an written to the event. +.. class:: RLock() + + The :class:`RLock` is a reentrant lock that can be used to protect access + or modification of variables by two cothreads at the same time. It is + reentrant because once it is acquired by a cothread, that same cothread + may acquire it again without blocking. This same cothread must release it + once for each time it has acquired it. + + It can be used as a context manager to acquire that lock and guarantee that + release will be called even if an exception is raised. For example:: + + lock = RLock() + x, y = 0, 0 + + with lock: + x = 1 + some_function_that_yields_control() + y = 1 + + Now as long as any other function that uses x and y also protects access + with this same lock, x and y will always be in a consistent state. It also + means that some_function_that_yields_control() can also acquire the lock + without causing a deadlock. + + The following methods are supported: + + .. method:: acquire(timeout=None) + + Acquire the lock if necessary and increment the recursion level. + + If this cothread already owns the lock, increment the recursion level + by one, and return immediately. Otherwise, if another cothread owns the + lock, block until the lock is unlocked. Once the lock is unlocked (not + owned by any cothread), then grab ownership, set the recursion level to + one, and return. If more than one thread is blocked waiting until the + lock is unlocked, only one at a time will be able to grab ownership of + the lock. + + .. method:: release() + + Release a lock, decrementing the recursion level + + If after the decrement it is zero, reset the lock to unlocked (not owned + by any cothread), and if any other cothreads are blocked waiting for the + lock to become unlocked, allow exactly one of them to proceed. If after + the decrement the recursion level is still nonzero, the lock remains + locked and owned by the calling cothread. + + Only call this method when the calling cothread owns the lock. An + AssertionError is raised if this method is called when the lock is + unlocked or the cothread doesn't own the lock. + + .. class:: Pulse() Pulse objects have no state and all cothreads waiting on a Pulse object will @@ -509,6 +563,34 @@ and :class:`EventQueue` objects. A :class:`Pulse` holds no values, an ``action()`` should return as soon as possible to avoid blocking subsequent callbacks -- if more work needs to be done, call ``Spawn()``. +.. function:: CallbackResult(action, *args, **kargs, \ + callback_queue=Callback, callback_timeout=None, callback_spawn=True) + + This is similar to :func:`Callback`: this can be called from any Python + thread, and ``action(*args, **kargs)`` will be called in cothread's own + thread. The difference is that the this function will block until + `action` returns, and the result will be returned as the result from + :func:`CallbackResult`. For example, the following can be used to perform + channel access from an arbitrary thread:: + + value = CallbackResult(caget, pv) + + The following arguments are processed by :func:`CallbackResult`, all others + are passed through to `action`: + + `callback_queue` + By default the standard :func:`Callback` queue is used for dispatch to + the cothread core, but a separate callback method can be specified here. + + `callback_timeout` + By default the thread will block indefinitely until `action` completes, + or a specific timeout can be specified here. + + `callback_spawn` + By default a new cothread will be spawned for each callback; this can + help to avoid interlock problems as mentioned above under + :func:`Callback`, but adds overhead. + .. function:: iqt(poll_interval=0.05, run_exec=True, argv=None) diff --git a/docs/training/.gitignore b/docs/training/.gitignore new file mode 100644 index 0000000..005121b --- /dev/null +++ b/docs/training/.gitignore @@ -0,0 +1 @@ +/cothread.html diff --git a/docs/training/Makefile b/docs/training/Makefile new file mode 100644 index 0000000..38e4cff --- /dev/null +++ b/docs/training/Makefile @@ -0,0 +1,18 @@ +RST2S5 = rst2s5.py + +# Need to explicitly point Python to our Pygments install +TOOLS = /dls_sw/prod/tools/RHEL6-x86_64 +LIB_PATH = prefix/lib/python2.7/site-packages +export PYTHONPATH = $(TOOLS)/Pygments/1-4/$(LIB_PATH)/Pygments-1.4-py2.7.egg + +%.html: %.rst + $(RST2S5) $< $@ + +default: cothread.html + +cothread.html: $(wildcard styles/*) + +clean: + rm -r cothread.html + +.PHONY: default clean diff --git a/docs/training/cothread.rst b/docs/training/cothread.rst new file mode 100644 index 0000000..2c6527b --- /dev/null +++ b/docs/training/cothread.rst @@ -0,0 +1,684 @@ +.. include:: + +.. role:: prettyprint + :class: prettyprint + +.. |emdash| unicode:: U+02014 .. EM DASH + + +================================ +Channel Access with ``cothread`` +================================ + +:Author: Michael Abbott + +Documentation: + .. class:: small + + file:///dls_sw/prod/common/python/RHEL6-x86_64/cothread/2-13/docs/html/index.html + + http://www.cs.diamond.ac.uk/docs/docshome/cothread/ + + +Cothread +======== + +The ``cothread`` library provides EPICS "Channel Access" bindings for Python. +The library comprises two parts: + +* ``cothread`` itself: "cooperative threads". + +* ``cothread.catools`` provides channel access bindings. + +Because EPICS involves communication with other machines events may happen at +any time. The ``cothread`` library provides a mechanism for managing these +updates with the minimum of interference with the rest of the program. + + +Cothread catools bindings +========================= + +The EPICS Channel Access Python interface consists of three functions: + +``caget(pvs, ...)`` + Retrieves value from a single PV or a list of PVs. + +``caput(pvs, values, ...)`` + Writes value or values to a single PV or list of PVs. + +``camonitor(pvs, callback, ...)`` + Creates "subscription" with updates every time a PV changes: ``callback`` is + called with a new value every time any listed PV updates. + + +Preliminaries +============= + +Need to import ``cothread``, all of the examples will start with +the following code, 2.13 is the current release: + +.. code:: python + + from pkg_resources import require + require('cothread==2.13') + + import cothread + from cothread.catools import * + +Channel access waveform data is returned as ``numpy`` arrays, so it will be +convenient to include this in our list of imports: + +.. code:: python + + import numpy + + +Example: Printing a PV +====================== + +Calling ``caget`` with a PV name returns the value of the PV. + +.. code:: python + + print caget('SR-DI-DCCT-01:SIGNAL') + +Calling ``caget`` with a list of PV names returns a list of PV values. + +.. code:: python + + bpms = ['SR%02dC-DI-EBPM-%02d:SA:X' % (c+1, n+1) + for c in range(24) for n in range(7)] + sax = caget(bpms) + print sax + +Note that calling ``caget`` with a long list is potentially *much* faster than +calling ``caget`` on each element of the list in turn, as passing a list to +``caget`` allows all fetches to proceed concurrently. + + +Testing Speed of ``caget`` +========================== + +Compare: + +.. code:: python + + import time + start = time.time(); caget(bpms); print time.time() - start + +with + +.. code:: python + + start = time.time(); [caget(bpm) for bpm in bpms]; print time.time() - start + +I tend to get a difference of a factor of around 50 between these two tests. + + +Exercise: Timing test +===================== + +Exercise: put everything in a file and run this test standalone. Try adding +more PVs and making the test controlled by a command line parameter. + + +Shape Polymorphism +================== + +As we've already seen, the behaviour of the three channel access functions +varies depending on whether the first argument is a string or a list of strings, +for example: + +.. code:: python + + print caget('SR-DI-DCCT-01:SIGNAL') + print caget(['SR-DI-DCCT-01:SIGNAL', 'SR-DI-DCCT-01:LIFETIME']) + +Similarly ``caput`` can write to multiple pvs, in which case a single value or a +list of values can be passed. + +.. code:: python + + caput(['TEST:PV1', 'TEST:PV2'], 3) # Writes 3 to both + caput(['TEST:PV1', 'TEST:S'], [10, 'testing']) # Writes different values + + # To write a repeated array to multiple TEST:PVs need to use repeat_value + caput(['TEST:WF1', 'TEST:WF2'], [1, 2], repeat_value = True) + +.. container:: incremental + + Note: this is the *only* case where ``repeat_value=True`` is needed. + + +Example: Monitoring a PV +======================== + +Updates to a PV result in a callback function being called. + +.. code:: python + + def print_update(value): + print value.name, value + + m = camonitor('SR-DI-DCCT-01:SIGNAL', print_update) + cothread.Sleep(10) + m.close() + +Updates arrive in the background until we close the monitor, but for normal +applications we leave the monitor open until the application exits. + + +``camonitor`` for Lists of PVs +============================== + +If ``camonitor`` is passed a list of PVs it expects the update function to take +a second argument which is used as an index. + +.. code:: python + + def print_update(value, index): + print value.name, index, value + + mm = camonitor(bpms, print_update) + cothread.Sleep(1) + for m in mm: + m.close() + +.. container:: incremental + + If the index is not needed there is no particular benefit to calling + ``camonitor`` on lists of PVs, unlike for ``caget`` and (it depends) + ``caput``. + + +Exercise: ``camonitor`` and ``caput`` +===================================== + +Use ``camonitor`` and ``caput`` to monitor ``TEST:PV1`` and add 1 to it after a +couple of seconds. + +Use ``cothread.Sleep(...)`` for sleeping. + +.. Warning:: + + Don't use ``time.sleep(...)`` when using cothread: this will prevent updates + from taking place! + +Use ``cothread.WaitForQuit()`` at the end of your script if there's nothing else +to do while cothread does its work. + + +A note on the last exercise +=========================== + +The obvious answer is to call ``cothread.Sleep`` in the camonitor callback +function, eg: + +.. code:: python + + def do_update(value): + cothread.Sleep(1) + caput(value.name, value + 1) + + m = camonitor('TEST:PV1', do_update) + cothread.Sleep(10) + m.close() + +Unfortunately doing this has the unfortunate side effect of blocking +all other camonitor updates during the ``Sleep``. + +Only one camonitor callback function is processed at a time. If you need to do +long lasting work in response to a PV update, push the processing somewhere else +with ``Spawn`` or ``Event``. + + +Augmented Values +================ + +Values returned by ``caget`` and delivered by ``camonitor`` are "augmented" by +extra information. The following two fields are always present: + +``.name`` + Contains the full name of the requested PV. + +``.ok`` + Will be ``True`` for values fetched without trouble, ``False`` if value is + not really a value! + +For example: + +.. code:: python + + v = caget('SR-DI-DCCT-01:SIGNAL') + print v.name, v.ok, v + + +Augmented Values are Ordinary +============================= + +Note that ``v`` (from above) is an ordinary number: + +.. code:: python + + print isinstance(v, float) + +However, it's not completely ordinary: + +.. code:: python + + print type(v), type(1.0) + +.. container:: incremental + + It will behave just like an ordinary float, but can be made completely + ordinary with the ``+`` operator: + + :: + + print +v, type(+v) + + However you should not normally need to use this! + + +Getting Values with Timestamps +============================== + +It is possible to get timestamp information with a retrieved or monitored PV, +but it needs to be requested: + +.. code:: python + + v = caget('SR-DI-DCCT-01:SIGNAL', format = FORMAT_TIME) + print v.name, v, v.datetime + +The timestamp is also available in raw Unix format (seconds since 1970): + +.. code:: python + + print v.timestamp + + +Example: Gathering Updates +========================== + +We'll monitor a requested number of updates and gather them into a list. For +this example the state is held in the local variables of the ``gather`` +function: note the use of a nested function. + +.. code:: python + + def gather(pv, count): + values = [] + done = cothread.Event() + def update(value): + values.append(value) + if len(values) >= count: + done.Signal() + m = camonitor(pv, update) + done.Wait() + m.close() + return values + + print gather('SR21C-DI-DCCT-01:SIGNAL', 10) + + +Example: Circular Updates Buffer +================================ + +Let's try a different version with a circular buffer. In this case we need a +class because the buffer will remain in existence for longer. + +.. code:: python + + class Gather: + def __init__(self, pv, count): + self.count = count + self.values = [0] * count + self.inptr = 0 + camonitor(pv, self.update) + def update(self, value): + self.values[self.inptr] = value + self.inptr = (self.inptr + 1) % self.count + def get(self): + return self.values[self.inptr:] + self.values[:self.inptr] + + buf = Gather('SR21C-DI-DCCT-01:SIGNAL', 10) + cothread.Sleep(1) + print buf.get() + cothread.Sleep(5) + print buf.get() + +This buffer can now safely be left running and will at any time return the last +``count`` values received. + + +Example: Gathering Arrays +========================= + +Working with ``numpy`` arrays can be much more efficient than working with +Python lists of values: + +.. code:: python + + x = numpy.array(caget(bpms)) + print x.mean(), x.std() + print x - x.mean() + +However, when it matters, timestamp information and other extended attributes +are lost when gathering values into arrays, so if the timestamps are needed a +little more care is required: + +.. code:: python + + rawx = caget(bpms, format = FORMAT_TIME) + x = numpy.array(rawx) + tx = numpy.array([v.timestamp for v in rawx]) + print tx.max() - tx.min() # Check spread of timestamps + + +Example: Default Error Handling +=============================== + +For ``caget`` we only really need to worry about fetching PVs that don't exist, +for ``camonitor`` we may also need to pay attention to PVs becoming +disconnected. The default behaviour of ``cothread`` produces sensible results, +but this can be overridden. + + +This behaviour of raising an exception when ``caget`` and ``caput`` fails is the +best default behaviour, because in routine naive use if a PV is unavailable then +this is an unrecoverable error. However, this isn't always what we want. + + +Adjusting the Timeout +===================== + +.. code:: python + + caget('bogus') + +This raises an exception after five seconds. The timeout can be adjusted with +an explicit argument: + +.. code:: python + + caget('bogus', timeout = 1) + +Alternatively, when fetching very large numbers of PVs through the gateway it +can happen that the default five second timeout isn't long enough. + + +Catching Errors from ``caget`` +============================== + +We can ask ``caget`` to return an error value instead of raising an exception: + +.. code:: python + + v1, v2 = caget(['bogus', 'SR-DI-DCCT-01:SIGNAL'], throw = False) + print v1.ok, v2.ok + +Note that if a pv is not ``ok`` we can't test for things like timestamps: + +.. code:: python + + v = caget('bogus', format = FORMAT_TIME, throw = False) + print v.name, v.ok + print v.datetime + +This raises an exception when trying to interrogate the ``datetime`` field on a +PV that never arrived! + + +Catching Errors from ``caput`` +============================== + +The same applies to ``caput``. We'll try writing to a PV we can write to, one +we can't, and one that doesn't exist: + +.. code:: python + + pvs = ['TEST:PV1', 'bogus', 'SR-DI-DCCT-01:SIGNAL'] + results = caput(pvs, 1, timeout = 1, throw = False) + for result in results: + print result.name, result.ok + print result + +Note that a complete description of the error is in the failing results: in this +case each result is a catchable exception object with a descriptive error +message as its string representation. + + +Cothread and Qt +=============== + +The cothread library relies on cooperative transfer of control between +cothreads, and similarly Qt has its own mechanism of events and notifications. +For cothread and Qt to work together, these two libraries need to be introduced +to each other. + +Fortunately this is easy. Run: + +.. code:: python + + cothread.iqt() + +It's safest to run this before importing any Qt dependent libraries. + +This function will create and return the Qt application object if you need it. + + +Plotting: Preamble +================== + +A certain amount of boilerplate preamble is required to get interactive plotting +working with ``dls-python``. We'll show the complete set: + +.. code:: python + + from pkg_resources import require + require('cothread==2.13') + require('matplotlib') + + import cothread + from cothread.catools import * + + import numpy + + cothread.iqt() + + import pylab + pylab.ion() + + +Plotting: An Example +==================== + +Now we can fetch and plot a waveform: + +.. code:: python + + wfx = caget('SR-DI-EBPM-01:SA:X') + ll = pylab.plot(wfx) + +Now let's make it update continuously: + +.. code:: python + + def update_ll(wfx): + ll[0].set_ydata(wfx) + pylab.draw() + + m = camonitor('SR-DI-EBPM-01:SA:X', update_ll) + +Exercise: Plot both X and Y on the same graph. Hint: ``SR-DI-EBPM-01:BPMID`` +contains a good x-axis. + + +Advanced Topics +=============== + + +Advanced Topic: Cothreads +========================= + +We've already seen cothreads: ``camonitor`` callbacks occur "in the background", +really they occur on a dedicated cothread. + +Cothreads are *cooperative* "threads", which means a cothread will run until it +deliberately relinquishes control. This has advantages and disadvantages: + +Advantage + No locking is required, a cothread will not run when it's not expected. + +Disadvantage + A cothread that won't relinquish control will block all other cothreads. + +Note that Python can't make use of multiple cores. + + +Creating a Cothread +=================== + +Creating a cothread is very easy: just define the function to run in the +cothread and call ``cothread.Spawn``: + +.. code:: python + + running = True + def background(name, sleep=1): + while running: + print 'Hello from', name + cothread.Sleep(sleep) + + cothread.Spawn(background, 'test1') + cothread.Spawn(background, 'test2', sleep = 2) + + cothread.Sleep(10) + running = False + + cothread.Sleep(3) + + +Communicating with Cothreads +============================ + +Communicate using event objects and queues: + +``cothread.Event()`` + Creates an event object which is either ready or not ready. When it's ready + it has a value. + +``cothread.EventQueue()`` + Almost exactly like an event object, but multiple values can be waiting. + +Both objects support two methods: + +event\ ``.Signal(value)`` + Makes event object ready with given value. + +event\ ``.Wait(timeout=None)`` + Suspends cothread until object is ready, consumes and returns value. + + +Example: Using ``Event`` +======================== + +.. code:: python + + class PV: + def __init__(self, pv): + self.event = cothread.Event() + camonitor(pv, self.__on_update, format = FORMAT_TIME) + def __on_update(self, value): + self.event.Signal(value) + def get(self, timeout=None): + return self.event.Wait(timeout) + + import time + pv = PV('SR21C-DI-DCCT-01:SIGNAL') + for n in range(5): + v = pv.get() + print v.timestamp, time.time() + cothread.Sleep(1) + +Note that we always get the latest value, even though the PV updates at 5Hz. + +Cothread already implements a fuller featured version of this class available as +``cothread.pv.PV``, and another variant ``cothread.pv.PV_array``. + + +Cothread Suspension Points +========================== + +The following functions are cothread suspension points (where control can be +yielded to another cothread): + +* ``Sleep()``, ``SleepUntil()``, ``Yield()`` +* event\ ``.Wait()`` +* ``caget()`` +* ``caput()`` most of the time (see documentation to avoid suspension). + +The following cothread modules provide extra cothread aware suspension points, +see documentation for details: + +* ``cothread.coselect``: provides ``select()`` and ``poll`` functionality. +* ``cothread.cosocket``: provides cothread aware socket API. + + +Cothreads and Real Threads +========================== + +Python threads are created with the ``threading.Thread`` class. A Python thread +cannot safely call cothread methods ... except for ``cothread.Callback()``, +which arranges for its argument to be called in a cothread: + +.. code:: python + + def callback_code(n): + print 'cothread tick', n + + import time + def thread_code(count): + for n in range(count): + print 'thread tick', n + cothread.Callback(callback_code, n) + time.sleep(0.5) + + import thread + thread.start_new_thread(thread_code, (5,)) + cothread.Sleep(5) + + +Slightly more Realistic Version +=============================== + +.. code:: python + + def consumer(event): + while True: + n = event.Wait() + print 'consumed', n + + import time + def producer(event, count): + for n in range(count): + print 'thread tick', n + # event.Signal(n) + cothread.Callback(lambda n: event.Signal(n), n) + time.sleep(0.5) + + import thread + event = cothread.Event() + cothread.Spawn(consumer, event) + thread.start_new_thread(producer, (event, 5)) + cothread.Sleep(5) + +Try replacing the ``Callback`` call with a direct ``Signal`` call and see what +happens. + +Bonus question: what's wrong with this? + +.. code:: python + + cothread.Callback(lambda: event.Signal(n)) diff --git a/docs/training/docutils.conf b/docs/training/docutils.conf new file mode 100644 index 0000000..a2b3b29 --- /dev/null +++ b/docs/training/docutils.conf @@ -0,0 +1,8 @@ +# Docutils configuration + +[html4css1 writer] +stylesheet-path: ./styles/html4css1.css,styles/pygments.css +embed-stylesheet: no + +[s5_html writer] +theme-url: ./styles diff --git a/docs/training/ioc.db b/docs/training/ioc.db new file mode 100644 index 0000000..71ce7fd --- /dev/null +++ b/docs/training/ioc.db @@ -0,0 +1,23 @@ +record(ai, "TEST:PV1") +{ +} + +record(ai, "TEST:PV2") +{ +} + +record(stringin, "TEST:S") +{ +} + +record(waveform, "TEST:WF1") +{ + field(NELM, 10) + field(FTVL, "FLOAT") +} + +record(waveform, "TEST:WF2") +{ + field(NELM, 10) + field(FTVL, "FLOAT") +} diff --git a/docs/training/styles/blank.gif b/docs/training/styles/blank.gif new file mode 100644 index 0000000..75b945d Binary files /dev/null and b/docs/training/styles/blank.gif differ diff --git a/docs/training/styles/dlsfooterpad.png b/docs/training/styles/dlsfooterpad.png new file mode 100644 index 0000000..ead6095 Binary files /dev/null and b/docs/training/styles/dlsfooterpad.png differ diff --git a/docs/training/styles/framing.css b/docs/training/styles/framing.css new file mode 100644 index 0000000..e4a194d --- /dev/null +++ b/docs/training/styles/framing.css @@ -0,0 +1,24 @@ +/* This file has been placed in the public domain. */ +/* The following styles size, place, and layer the slide components. + Edit these if you want to change the overall slide layout. + The commented lines can be uncommented (and modified, if necessary) + to help you with the rearrangement process. */ + +/* target = 1024x768 */ + +div#header, div#footer, .slide {width: 100%; top: 0; left: 0;} +div#footer {top: auto; bottom: 0; height: 2.5em; z-index: 0;} +.slide {top: 0; width: 92%; padding: 1em 4% 0 4%; z-index: 2;} +div#controls {left: 50%; bottom: 0; width: 50%; z-index: 100;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0;} +#currentSlide {position: absolute; width: 10%; left: 45%; bottom: 1em; + z-index: 10;} +html>body #currentSlide {position: fixed;} + +/* +div#header {background: #FCC;} +div#footer {background: #CCF;} +div#controls {background: #BBD;} +div#currentSlide {background: #FFC;} +*/ diff --git a/docs/training/styles/html4css1.css b/docs/training/styles/html4css1.css new file mode 100644 index 0000000..f5285ad --- /dev/null +++ b/docs/training/styles/html4css1.css @@ -0,0 +1,311 @@ +/* +:Author: David Goodger (goodger@python.org) +:Id: $Id: html4css1.css 7434 2012-05-11 21:06:27Z milde $ +:Copyright: This stylesheet has been placed in the public domain. + +Default cascading style sheet for the HTML output of Docutils. + +See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to +customize this style sheet. +*/ + +/* used to remove borders from tables and images */ +.borderless, table.borderless td, table.borderless th { + border: 0 } + +table.borderless td, table.borderless th { + /* Override padding for "table.docutils td" with "! important". + The right padding separates the table cells. */ + padding: 0 0.5em 0 0 ! important } + +.first { + /* Override more specific margin styles with "! important". */ + margin-top: 0 ! important } + +.last, .with-subtitle { + margin-bottom: 0 ! important } + +.hidden { + display: none } + +a.toc-backref { + text-decoration: none ; + color: black } + +blockquote.epigraph { + margin: 2em 5em ; } + +dl.docutils dd { + margin-bottom: 0.5em } + +object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { + overflow: hidden; +} + +/* Uncomment (and remove this text!) to get bold-faced definition list terms +dl.docutils dt { + font-weight: bold } +*/ + +div.abstract { + margin: 2em 5em } + +div.abstract p.topic-title { + font-weight: bold ; + text-align: center } + +div.admonition, div.attention, div.caution, div.danger, div.error, +div.hint, div.important, div.note, div.tip, div.warning { + margin: 2em ; + border: medium outset ; + padding: 1em } + +div.admonition p.admonition-title, div.hint p.admonition-title, +div.important p.admonition-title, div.note p.admonition-title, +div.tip p.admonition-title { + font-weight: bold ; + font-family: sans-serif } + +div.attention p.admonition-title, div.caution p.admonition-title, +div.danger p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title { + color: red ; + font-weight: bold ; + font-family: sans-serif } + +/* Uncomment (and remove this text!) to get reduced vertical space in + compound paragraphs. +div.compound .compound-first, div.compound .compound-middle { + margin-bottom: 0.5em } + +div.compound .compound-last, div.compound .compound-middle { + margin-top: 0.5em } +*/ + +div.dedication { + margin: 2em 5em ; + text-align: center ; + font-style: italic } + +div.dedication p.topic-title { + font-weight: bold ; + font-style: normal } + +div.figure { + margin-left: 2em ; + margin-right: 2em } + +div.footer, div.header { + clear: both; + font-size: smaller } + +div.line-block { + display: block ; + margin-top: 1em ; + margin-bottom: 1em } + +div.line-block div.line-block { + margin-top: 0 ; + margin-bottom: 0 ; + margin-left: 1.5em } + +div.sidebar { + margin: 0 0 0.5em 1em ; + border: medium outset ; + padding: 1em ; + background-color: #ffffee ; + width: 40% ; + float: right ; + clear: right } + +div.sidebar p.rubric { + font-family: sans-serif ; + font-size: medium } + +div.system-messages { + margin: 5em } + +div.system-messages h1 { + color: red } + +div.system-message { + border: medium outset ; + padding: 1em } + +div.system-message p.system-message-title { + color: red ; + font-weight: bold } + +div.topic { + margin: 2em } + +h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, +h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { + margin-top: 0.4em } + +h1.title { + text-align: center } + +h2.subtitle { + text-align: center } + +hr.docutils { + width: 75% } + +img.align-left, .figure.align-left, object.align-left { + clear: left ; + float: left ; + margin-right: 1em } + +img.align-right, .figure.align-right, object.align-right { + clear: right ; + float: right ; + margin-left: 1em } + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left } + +.align-center { + clear: both ; + text-align: center } + +.align-right { + text-align: right } + +/* reset inner alignment in figures */ +div.align-right { + text-align: inherit } + +/* div.align-center * { */ +/* text-align: left } */ + +ol.simple, ul.simple { + margin-bottom: 1em } + +ol.arabic { + list-style: decimal } + +ol.loweralpha { + list-style: lower-alpha } + +ol.upperalpha { + list-style: upper-alpha } + +ol.lowerroman { + list-style: lower-roman } + +ol.upperroman { + list-style: upper-roman } + +p.attribution { + text-align: right ; + margin-left: 50% } + +p.caption { + font-style: italic } + +p.credits { + font-style: italic ; + font-size: smaller } + +p.label { + white-space: nowrap } + +p.rubric { + font-weight: bold ; + font-size: larger ; + color: maroon ; + text-align: center } + +p.sidebar-title { + font-family: sans-serif ; + font-weight: bold ; + font-size: larger } + +p.sidebar-subtitle { + font-family: sans-serif ; + font-weight: bold } + +p.topic-title { + font-weight: bold } + +pre.address { + margin-bottom: 0 ; + margin-top: 0 ; + font: inherit } + +pre.literal-block, pre.doctest-block, pre.math, pre.code { + margin-left: 2em ; + margin-right: 2em } + +pre.code .ln { /* line numbers */ + color: grey; +} + +.code { + background-color: #eeeeee +} + +span.classifier { + font-family: sans-serif ; + font-style: oblique } + +span.classifier-delimiter { + font-family: sans-serif ; + font-weight: bold } + +span.interpreted { + font-family: sans-serif } + +span.option { + white-space: nowrap } + +span.pre { + white-space: pre } + +span.problematic { + color: red } + +span.section-subtitle { + /* font-size relative to parent (h1..h6 element) */ + font-size: 80% } + +table.citation { + border-left: solid 1px gray; + margin-left: 1px } + +table.docinfo { + margin: 2em 4em } + +table.docutils { + margin-top: 0.5em ; + margin-bottom: 0.5em } + +table.footnote { + border-left: solid 1px black; + margin-left: 1px } + +table.docutils td, table.docutils th, +table.docinfo td, table.docinfo th { + padding-left: 0.5em ; + padding-right: 0.5em ; + vertical-align: top } + +table.docutils th.field-name, table.docinfo th.docinfo-name { + font-weight: bold ; + text-align: left ; + white-space: nowrap ; + padding-left: 0 } + +h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, +h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { + font-size: 100% } + +ul.auto-toc { + list-style-type: none } diff --git a/docs/training/styles/iepngfix.htc b/docs/training/styles/iepngfix.htc new file mode 100644 index 0000000..2a44681 --- /dev/null +++ b/docs/training/styles/iepngfix.htc @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/docs/training/styles/opera.css b/docs/training/styles/opera.css new file mode 100644 index 0000000..c9d1148 --- /dev/null +++ b/docs/training/styles/opera.css @@ -0,0 +1,8 @@ +/* This file has been placed in the public domain. */ +/* DO NOT CHANGE THESE unless you really want to break Opera Show */ +.slide { + visibility: visible !important; + position: static !important; + page-break-before: always; +} +#slide0 {page-break-before: avoid;} diff --git a/docs/training/styles/outline.css b/docs/training/styles/outline.css new file mode 100644 index 0000000..fa767e2 --- /dev/null +++ b/docs/training/styles/outline.css @@ -0,0 +1,16 @@ +/* This file has been placed in the public domain. */ +/* Don't change this unless you want the layout stuff to show up in the + outline view! */ + +.layout div, #footer *, #controlForm * {display: none;} +#footer, #controls, #controlForm, #navLinks, #toggle { + display: block; visibility: visible; margin: 0; padding: 0;} +#toggle {float: right; padding: 0.5em;} +html>body #toggle {position: fixed; top: 0; right: 0;} + +/* making the outline look pretty-ish */ + +#slide0 h1, #slide0 h2, #slide0 h3, #slide0 h4 {border: none; margin: 0;} +#toggle {border: 1px solid; border-width: 0 0 1px 1px; background: #FFF;} + +.outline {display: inline ! important;} diff --git a/docs/training/styles/pretty.css b/docs/training/styles/pretty.css new file mode 100644 index 0000000..e495a84 --- /dev/null +++ b/docs/training/styles/pretty.css @@ -0,0 +1,126 @@ +/* This file has been placed in the public domain. */ +/* Following are the presentation styles -- edit away! */ + +html, body {margin: 0; padding: 0;} +body {background: white; color: black;} +:link, :visited {text-decoration: none; color: #00C;} +#controls :active {color: #888 !important;} +#controls :focus {outline: 1px dotted #222;} +h1, h2, h3, h4 {font-size: 100%; margin: 0; padding: 0; font-weight: inherit;} + +blockquote {padding: 0 2em 0.5em; margin: 0 1.5em 0.5em;} +blockquote p {margin: 0;} + +kbd {font-weight: bold; font-size: 1em;} +sup {font-size: smaller; line-height: 1px;} + +.slide pre {padding: 0; margin-left: 0; margin-right: 0; font-size: 60%;} +.slide ul ul li {list-style: square;} +.slide img.leader {display: block; margin: 0 auto;} +.slide tt {font-size: 90%;} + +div#footer {font-family: sans-serif; color: #444; + font-size: 0.5em; font-weight: bold; padding: 1em 0;} +#footer h1 {display: block; padding: 0 1em;} +#footer h2 {display: block; padding: 0.8em 1em 0;} + +/* This line determines the relative sizes of text. */ +.slide {font-size: 1em;} + +div#header { + height: 100%; + background-image: url('dlsfooterpad.png'); + background-repeat: no-repeat; + background-position: bottom; + background-size: 100%; +} + +.slide h1 {padding-top: 0; z-index: 1; margin: 0; font: bold 150% sans-serif;} +.slide h2 {font: bold 120% sans-serif; padding-top: 0.5em;} +.slide h3 {font: bold 100% sans-serif; padding-top: 0.5em;} +h1 abbr {font-variant: small-caps;} + +div#controls {position: absolute; left: 50%; bottom: 0; + width: 50%; text-align: right; font: bold 0.9em sans-serif;} +html>body div#controls {position: fixed; padding: 0 0 1em 0; top: auto;} +div#controls form {position: absolute; bottom: 0; right: 0; width: 100%; + margin: 0; padding: 0;} +#controls #navLinks a {padding: 0; margin: 0 0.5em; + border: none; color: #888; cursor: pointer;} +#controls #navList {height: 1em;} +#controls #navList #jumplist {position: absolute; bottom: 0; right: 0; + background: #DDD; color: #222;} + +#currentSlide {text-align: center; font-size: 0.5em; color: #444; + font-family: sans-serif; font-weight: bold;} + +#slide0 {padding-top: 0em} +#slide0 h1 {position: static; margin: 1em 0 0; padding: 0; + font: bold 2em sans-serif; white-space: normal; background: transparent;} +#slide0 h2 {font: bold italic 1em sans-serif; margin: 0.25em;} +#slide0 h3 {margin-top: 1.5em; font-size: 1.5em;} +#slide0 h4 {margin-top: 0; font-size: 1em;} + +ul.urls {list-style: none; display: inline; margin: 0;} +.urls li {display: inline; margin: 0;} +.external {border-bottom: 1px dotted gray;} +html>body .external {border-bottom: none;} +.external:after {content: " \274F"; font-size: smaller; color: #77B;} + +.incremental, .incremental *, .incremental *:after { + color: transparent; visibility: visible; border: 0; border: 0;} +img.incremental {visibility: hidden;} +.slide .current {color: black;} + +.slide-display {display: inline ! important;} + +.huge {font-family: sans-serif; font-weight: bold; font-size: 150%;} +.big {font-family: sans-serif; font-weight: bold; font-size: 120%;} +.small {font-size: 75%;} +.tiny {font-size: 50%;} +.huge tt, .big tt, .small tt, .tiny tt {font-size: 115%;} +.huge pre, .big pre, .small pre, .tiny pre {font-size: 115%;} + +.maroon {color: maroon;} +.red {color: red;} +.magenta {color: magenta;} +.fuchsia {color: fuchsia;} +.pink {color: #FAA;} +.orange {color: orange;} +.yellow {color: yellow;} +.lime {color: lime;} +.green {color: green;} +.olive {color: olive;} +.teal {color: teal;} +.cyan {color: cyan;} +.aqua {color: aqua;} +.blue {color: blue;} +.navy {color: navy;} +.purple {color: purple;} +.black {color: black;} +.gray {color: gray;} +.silver {color: silver;} +.white {color: white;} + +.left {text-align: left ! important;} +.center {text-align: center ! important;} +.right {text-align: right ! important;} + +.animation {position: relative; margin: 1em 0; padding: 0;} +.animation img {position: absolute;} + +/* Docutils-specific overrides */ + +.slide table.docinfo {margin: 1em 0 0.5em 2em;} + +/* +pre.literal-block, pre.doctest-block {background-color: white;} + +tt.docutils {background-color: white;} +*/ + +/* diagnostics */ +/* +li:after {content: " [" attr(class) "]"; color: #F88;} +div:before {content: "[" attr(class) "]"; color: #F88;} +*/ diff --git a/docs/training/styles/print.css b/docs/training/styles/print.css new file mode 100644 index 0000000..9d057cc --- /dev/null +++ b/docs/training/styles/print.css @@ -0,0 +1,24 @@ +/* This file has been placed in the public domain. */ +/* The following rule is necessary to have all slides appear in print! + DO NOT REMOVE IT! */ +.slide, ul {page-break-inside: avoid; visibility: visible !important;} +h1 {page-break-after: avoid;} + +body {font-size: 12pt; background: white;} +* {color: black;} + +#slide0 h1 {font-size: 200%; border: none; margin: 0.5em 0 0.25em;} +#slide0 h3 {margin: 0; padding: 0;} +#slide0 h4 {margin: 0 0 0.5em; padding: 0;} +#slide0 {margin-bottom: 3em;} + +#header {display: none;} +#footer h1 {margin: 0; border-bottom: 1px solid; color: gray; + font-style: italic;} +#footer h2, #controls {display: none;} + +.print {display: inline ! important;} + +/* The following rule keeps the layout stuff out of print. + Remove at your own risk! */ +.layout, .layout * {display: none !important;} diff --git a/docs/training/styles/pygments.css b/docs/training/styles/pygments.css new file mode 100644 index 0000000..b8ec0e3 --- /dev/null +++ b/docs/training/styles/pygments.css @@ -0,0 +1,62 @@ +.code .hll { background-color: #ffffcc } +.code { background: rgba(238, 255, 204, 0.6); } /* #eeffcc */ +.code .c { color: #408090; font-style: italic } /* Comment */ +.code .err { border: 1px solid #FF0000 } /* Error */ +.code .k { color: #007020; font-weight: bold } /* Keyword */ +.code .o { color: #666666 } /* Operator */ +.code .cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.code .cp { color: #007020 } /* Comment.Preproc */ +.code .c1 { color: #408090; font-style: italic } /* Comment.Single */ +.code .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ +.code .gd { color: #A00000 } /* Generic.Deleted */ +.code .ge { font-style: italic } /* Generic.Emph */ +.code .gr { color: #FF0000 } /* Generic.Error */ +.code .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.code .gi { color: #00A000 } /* Generic.Inserted */ +.code .go { color: #303030 } /* Generic.Output */ +.code .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.code .gs { font-weight: bold } /* Generic.Strong */ +.code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.code .gt { color: #0040D0 } /* Generic.Traceback */ +.code .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.code .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.code .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.code .kp { color: #007020 } /* Keyword.Pseudo */ +.code .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.code .kt { color: #902000 } /* Keyword.Type */ +.code .m { color: #208050 } /* Literal.Number */ +.code .s { color: #4070a0 } /* Literal.String */ +.code .na { color: #4070a0 } /* Name.Attribute */ +.code .nb { color: #007020 } /* Name.Builtin */ +.code .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ +.code .no { color: #60add5 } /* Name.Constant */ +.code .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.code .ni { color: #d55537; font-weight: bold } /* Name.Entity */ +.code .ne { color: #007020 } /* Name.Exception */ +.code .nf { color: #06287e } /* Name.Function */ +.code .nl { color: #002070; font-weight: bold } /* Name.Label */ +.code .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.code .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.code .nv { color: #bb60d5 } /* Name.Variable */ +.code .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.code .w { color: #bbbbbb } /* Text.Whitespace */ +.code .mf { color: #208050 } /* Literal.Number.Float */ +.code .mh { color: #208050 } /* Literal.Number.Hex */ +.code .mi { color: #208050 } /* Literal.Number.Integer */ +.code .mo { color: #208050 } /* Literal.Number.Oct */ +.code .sb { color: #4070a0 } /* Literal.String.Backtick */ +.code .sc { color: #4070a0 } /* Literal.String.Char */ +.code .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ +.code .s2 { color: #4070a0 } /* Literal.String.Double */ +.code .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ +.code .sh { color: #4070a0 } /* Literal.String.Heredoc */ +.code .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ +.code .sx { color: #c65d09 } /* Literal.String.Other */ +.code .sr { color: #235388 } /* Literal.String.Regex */ +.code .s1 { color: #4070a0 } /* Literal.String.Single */ +.code .ss { color: #517918 } /* Literal.String.Symbol */ +.code .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.code .vc { color: #bb60d5 } /* Name.Variable.Class */ +.code .vg { color: #bb60d5 } /* Name.Variable.Global */ +.code .vi { color: #bb60d5 } /* Name.Variable.Instance */ +.code .il { color: #208050 } /* Literal.Number.Integer.Long */ diff --git a/docs/training/styles/s5-core.css b/docs/training/styles/s5-core.css new file mode 100644 index 0000000..6965f5e --- /dev/null +++ b/docs/training/styles/s5-core.css @@ -0,0 +1,11 @@ +/* This file has been placed in the public domain. */ +/* Do not edit or override these styles! + The system will likely break if you do. */ + +div#header, div#footer, div#controls, .slide {position: absolute;} +html>body div#header, html>body div#footer, + html>body div#controls, html>body .slide {position: fixed;} +.handout {display: none;} +.layout {display: block;} +.slide, .hideme, .incremental {visibility: hidden;} +#slide0 {visibility: visible;} diff --git a/docs/training/styles/slides.css b/docs/training/styles/slides.css new file mode 100644 index 0000000..82bdc0e --- /dev/null +++ b/docs/training/styles/slides.css @@ -0,0 +1,10 @@ +/* This file has been placed in the public domain. */ + +/* required to make the slide show run at all */ +@import url(s5-core.css); + +/* sets basic placement and size of slide components */ +@import url(framing.css); + +/* styles that make the slides look good */ +@import url(pretty.css); diff --git a/docs/training/styles/slides.js b/docs/training/styles/slides.js new file mode 100644 index 0000000..bcd28ab --- /dev/null +++ b/docs/training/styles/slides.js @@ -0,0 +1,544 @@ +// S5 v1.1 slides.js -- released into the Public Domain +// Modified for Docutils (http://docutils.sf.net) by David Goodger +// +// Please see http://www.meyerweb.com/eric/tools/s5/credits.html for +// information about all the wonderful and talented contributors to this code! + +var undef; +var slideCSS = ''; +var snum = 0; +var smax = 1; +var slideIDs = new Array(); +var incpos = 0; +var number = undef; +var s5mode = true; +var defaultView = 'slideshow'; +var controlVis = 'visible'; + +var isIE = navigator.appName == 'Microsoft Internet Explorer' ? 1 : 0; +var isOp = navigator.userAgent.indexOf('Opera') > -1 ? 1 : 0; +var isGe = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('Safari') < 1 ? 1 : 0; + +function hasClass(object, className) { + if (!object.className) return false; + return (object.className.search('(^|\\s)' + className + '(\\s|$)') != -1); +} + +function hasValue(object, value) { + if (!object) return false; + return (object.search('(^|\\s)' + value + '(\\s|$)') != -1); +} + +function removeClass(object,className) { + if (!object) return; + object.className = object.className.replace(new RegExp('(^|\\s)'+className+'(\\s|$)'), RegExp.$1+RegExp.$2); +} + +function addClass(object,className) { + if (!object || hasClass(object, className)) return; + if (object.className) { + object.className += ' '+className; + } else { + object.className = className; + } +} + +function GetElementsWithClassName(elementName,className) { + var allElements = document.getElementsByTagName(elementName); + var elemColl = new Array(); + for (var i = 0; i< allElements.length; i++) { + if (hasClass(allElements[i], className)) { + elemColl[elemColl.length] = allElements[i]; + } + } + return elemColl; +} + +function isParentOrSelf(element, id) { + if (element == null || element.nodeName=='BODY') return false; + else if (element.id == id) return true; + else return isParentOrSelf(element.parentNode, id); +} + +function nodeValue(node) { + var result = ""; + if (node.nodeType == 1) { + var children = node.childNodes; + for (var i = 0; i < children.length; ++i) { + result += nodeValue(children[i]); + } + } + else if (node.nodeType == 3) { + result = node.nodeValue; + } + return(result); +} + +function slideLabel() { + var slideColl = GetElementsWithClassName('*','slide'); + var list = document.getElementById('jumplist'); + smax = slideColl.length; + for (var n = 0; n < smax; n++) { + var obj = slideColl[n]; + + var did = 'slide' + n.toString(); + if (obj.getAttribute('id')) { + slideIDs[n] = obj.getAttribute('id'); + } + else { + obj.setAttribute('id',did); + slideIDs[n] = did; + } + if (isOp) continue; + + var otext = ''; + var menu = obj.firstChild; + if (!menu) continue; // to cope with empty slides + while (menu && menu.nodeType == 3) { + menu = menu.nextSibling; + } + if (!menu) continue; // to cope with slides with only text nodes + + var menunodes = menu.childNodes; + for (var o = 0; o < menunodes.length; o++) { + otext += nodeValue(menunodes[o]); + } + list.options[list.length] = new Option(n + ' : ' + otext, n); + } +} + +function currentSlide() { + var cs; + var footer_nodes; + var vis = 'visible'; + if (document.getElementById) { + cs = document.getElementById('currentSlide'); + footer_nodes = document.getElementById('footer').childNodes; + } else { + cs = document.currentSlide; + footer = document.footer.childNodes; + } + cs.innerHTML = '' + snum + '<\/span> ' + + '\/<\/span> ' + + '' + (smax-1) + '<\/span>'; + if (snum == 0) { + vis = 'hidden'; + } + cs.style.visibility = vis; + for (var i = 0; i < footer_nodes.length; i++) { + if (footer_nodes[i].nodeType == 1) { + footer_nodes[i].style.visibility = vis; + } + } +} + +function go(step) { + if (document.getElementById('slideProj').disabled || step == 0) return; + var jl = document.getElementById('jumplist'); + var cid = slideIDs[snum]; + var ce = document.getElementById(cid); + if (incrementals[snum].length > 0) { + for (var i = 0; i < incrementals[snum].length; i++) { + removeClass(incrementals[snum][i], 'current'); + removeClass(incrementals[snum][i], 'incremental'); + } + } + if (step != 'j') { + snum += step; + lmax = smax - 1; + if (snum > lmax) snum = lmax; + if (snum < 0) snum = 0; + } else + snum = parseInt(jl.value); + var nid = slideIDs[snum]; + var ne = document.getElementById(nid); + if (!ne) { + ne = document.getElementById(slideIDs[0]); + snum = 0; + } + if (step < 0) {incpos = incrementals[snum].length} else {incpos = 0;} + if (incrementals[snum].length > 0 && incpos == 0) { + for (var i = 0; i < incrementals[snum].length; i++) { + if (hasClass(incrementals[snum][i], 'current')) + incpos = i + 1; + else + addClass(incrementals[snum][i], 'incremental'); + } + } + if (incrementals[snum].length > 0 && incpos > 0) + addClass(incrementals[snum][incpos - 1], 'current'); + ce.style.visibility = 'hidden'; + ne.style.visibility = 'visible'; + jl.selectedIndex = snum; + currentSlide(); + number = 0; +} + +function goTo(target) { + if (target >= smax || target == snum) return; + go(target - snum); +} + +function subgo(step) { + if (step > 0) { + removeClass(incrementals[snum][incpos - 1],'current'); + removeClass(incrementals[snum][incpos], 'incremental'); + addClass(incrementals[snum][incpos],'current'); + incpos++; + } else { + incpos--; + removeClass(incrementals[snum][incpos],'current'); + addClass(incrementals[snum][incpos], 'incremental'); + addClass(incrementals[snum][incpos - 1],'current'); + } +} + +function toggle() { + var slideColl = GetElementsWithClassName('*','slide'); + var slides = document.getElementById('slideProj'); + var outline = document.getElementById('outlineStyle'); + if (!slides.disabled) { + slides.disabled = true; + outline.disabled = false; + s5mode = false; + fontSize('1em'); + for (var n = 0; n < smax; n++) { + var slide = slideColl[n]; + slide.style.visibility = 'visible'; + } + } else { + slides.disabled = false; + outline.disabled = true; + s5mode = true; + fontScale(); + for (var n = 0; n < smax; n++) { + var slide = slideColl[n]; + slide.style.visibility = 'hidden'; + } + slideColl[snum].style.visibility = 'visible'; + } +} + +function showHide(action) { + var obj = GetElementsWithClassName('*','hideme')[0]; + switch (action) { + case 's': obj.style.visibility = 'visible'; break; + case 'h': obj.style.visibility = 'hidden'; break; + case 'k': + if (obj.style.visibility != 'visible') { + obj.style.visibility = 'visible'; + } else { + obj.style.visibility = 'hidden'; + } + break; + } +} + +// 'keys' code adapted from MozPoint (http://mozpoint.mozdev.org/) +function keys(key) { + if (!key) { + key = event; + key.which = key.keyCode; + } + if (key.which == 84) { + toggle(); + return; + } + if (s5mode) { + switch (key.which) { + case 10: // return + case 13: // enter + if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return; + if (key.target && isParentOrSelf(key.target, 'controls')) return; + if(number != undef) { + goTo(number); + break; + } + case 32: // spacebar + case 34: // page down + case 39: // rightkey + case 40: // downkey + case 74: // 'j' + case 78: // 'n' + if(number != undef) { + go(number); + } else if (!incrementals[snum] || incpos >= incrementals[snum].length) { + go(1); + } else { + subgo(1); + } + break; + case 33: // page up + case 37: // leftkey + case 38: // upkey + case 75: // 'k' + case 80: // 'p' + if(number != undef) { + go(-1 * number); + } else if (!incrementals[snum] || incpos <= 0) { + go(-1); + } else { + subgo(-1); + } + break; + case 36: // home + goTo(0); + break; + case 35: // end + goTo(smax-1); + break; + case 67: // c + showHide('k'); + break; + } + if (key.which < 48 || key.which > 57) { + number = undef; + } else { + if (window.event && isParentOrSelf(window.event.srcElement, 'controls')) return; + if (key.target && isParentOrSelf(key.target, 'controls')) return; + number = (((number != undef) ? number : 0) * 10) + (key.which - 48); + } + } + return false; +} + +function findSlide(hash) { + var target = document.getElementById(hash); + if (target) { + for (var i = 0; i < slideIDs.length; i++) { + if (target.id == slideIDs[i]) return i; + } + } + return null; +} + +function slideJump() { + if (window.location.hash == null || window.location.hash == '') { + currentSlide(); + return; + } + if (window.location.hash == null) return; + var dest = null; + dest = findSlide(window.location.hash.slice(1)); + if (dest == null) { + dest = 0; + } + go(dest - snum); +} + +function fixLinks() { + var thisUri = window.location.href; + thisUri = thisUri.slice(0, thisUri.length - window.location.hash.length); + var aelements = document.getElementsByTagName('A'); + for (var i = 0; i < aelements.length; i++) { + var a = aelements[i].href; + var slideID = a.match('\#.+'); + if ((slideID) && (slideID[0].slice(0,1) == '#')) { + var dest = findSlide(slideID[0].slice(1)); + if (dest != null) { + if (aelements[i].addEventListener) { + aelements[i].addEventListener("click", new Function("e", + "if (document.getElementById('slideProj').disabled) return;" + + "go("+dest+" - snum); " + + "if (e.preventDefault) e.preventDefault();"), true); + } else if (aelements[i].attachEvent) { + aelements[i].attachEvent("onclick", new Function("", + "if (document.getElementById('slideProj').disabled) return;" + + "go("+dest+" - snum); " + + "event.returnValue = false;")); + } + } + } + } +} + +function externalLinks() { + if (!document.getElementsByTagName) return; + var anchors = document.getElementsByTagName('a'); + for (var i=0; i' + + '