Skip to content

Commit

Permalink
Issue ChristianTremblay#103: util.state: Re-structure for pre-3.3 Pyt…
Browse files Browse the repository at this point in the history
…hon.

Python older than v3.3 does not understand `yield from`, and won't
ignore it inspite of it being in a code path that won't be run for older
releases.

So instead, we have to put it in a completely separate file and convince
Python to hopefully not import it when running on older Python releases.
  • Loading branch information
sjlongland committed Nov 24, 2020
1 parent 0c057d8 commit faa5f91
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 181 deletions.
21 changes: 21 additions & 0 deletions pyhaystack/util/awaitableop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
"""
State machine interface. This is a base class for implementing state machines.
"""

from .operation import BaseHaystackOperation
from collections.abc import Awaitable


class HaystackOperation(BaseHaystackOperation, Awaitable):
"""
Awaitable version of BaseHaystackOperation. This is provided for later
versions of Python 3 (3.5 and up) that support the `await` keyword.
"""
def __await__(self):
"""
Return a future object which can be awaited by asyncio-aware
tools like ipython and in asynchronous scripts.
"""
res = yield from self.future
return res
178 changes: 178 additions & 0 deletions pyhaystack/util/operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# -*- coding: utf-8 -*-
"""
State machine interface. This is a base class for implementing state machines.
"""

from copy import deepcopy
from signalslot import Signal
from threading import Event

from .asyncexc import AsynchronousException

# Support for asyncio
try:
from asyncio.futures import Future

HAVE_FUTURE = 'asyncio'
except ImportError:
HAVE_FUTURE = None

if HAVE_FUTURE is None:
# Try Tornado
try:
from tornado.concurrent import Future
HAVE_FUTURE = 'tornado'
except ImportError:
pass


class NotReadyError(Exception):
"""
Exception raised when an attempt is made to retrieve the result of an
operation before it is ready.
"""
pass


class BaseHaystackOperation(object):
"""
A core state machine object. This implements the basic interface presented
for all operations in pyhaystack.
"""

def __init__(self, result_copy=True, result_deepcopy=True):
"""
Initialisation. This should be overridden by subclasses to accept and
validate the inputs presented for the operation, raising an appropriate
Exception subclass if the inputs are found to be invalid.
These should be stored here by the initialisation function as private
variables in suitably sanitised form. The core state machine object
shall then be created and stored before the object is returned to the
caller.
"""
# Event object to represent when this operation is "done"
self._done_evt = Event()

# Signal emitted when the operation is "done"
self.done_sig = Signal(name="done", threadsafe=True)

# Result returned by operation
self._result = None
self._result_copy = result_copy
self._result_deepcopy = result_deepcopy

def go(self):
"""
Start processing the operation. This is called by the caller (so after
all __init__ functions have executed) in order to begin the asynchronous
operation.
"""
# This needs to be implemented in the subclass.
raise NotImplementedError(
"To be implemented in subclass %s" % self.__class__.__name__
)

def wait(self, timeout=None):
"""
Wait for an operation to finish. This should *NOT* be called in the
same thread as the thread executing the operation as this will
deadlock.
"""
self._done_evt.wait(timeout)

@property
def future(self):
"""
Return a Future object (asyncio or Tornado).
"""
if HAVE_FUTURE is None:
raise NotImplementedError(
'Futures require either asyncio and/or Tornado (>=4) to work'
)

# Both Tornado and asyncio future classes work the same.
future = Future()
if self.is_done:
self._set_future(future)
else:
# Not done yet, wait for it
def _on_done(*a, **kwa):
self._set_future(future)
self.done_sig.connect(_on_done)

# Return the future for the caller
return future

@property
def state(self):
"""
Return the current state machine's state.
"""
return self._state_machine.current

@property
def is_done(self):
"""
Return true if the operation is complete.
"""
return self._state_machine.is_finished()

@property
def is_failed(self):
"""
Return true if the result is an Exception.
"""
return isinstance(self._result, AsynchronousException)

@property
def result(self):
"""
Return the result of the operation or raise its exception.
Raises NotReadyError if not ready.
"""
if not self.is_done:
raise NotReadyError()

if self.is_failed:
self._result.reraise()

if not self._result_copy:
# Return the original instance (do not copy)
return self._result
elif self._result_deepcopy:
# Return a deep copy
return deepcopy(self._result)
else:
# Return a shallow copy
return self._result.copy()

def __repr__(self):
"""
Return a representation of this object's state.
"""
if self.is_failed:
return "<%s failed>" % self.__class__.__name__
elif self.is_done:
return "<%s done: %s>" % (self.__class__.__name__, self._result)
else:
return "<%s %s>" % (self.__class__.__name__, self.state)

def _done(self, result):
"""
Return the result of the operation to any listeners.
"""
self._result = result
self._done_evt.set()
self.done_sig.emit(operation=self)

def _set_future(self, future):
"""
Set the given future to the operation result, if known
or raise an exception otherwise.
"""
# It's already done
try:
future.set_result(self.result)
except Exception as e:
future.set_exception(e)
189 changes: 8 additions & 181 deletions pyhaystack/util/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,189 +3,16 @@
State machine interface. This is a base class for implementing state machines.
"""

from copy import deepcopy
from signalslot import Signal
from threading import Event

from .operation import HAVE_FUTURE, NotReadyError, BaseHaystackOperation
from .asyncexc import AsynchronousException

# Support for asyncio
try:
from collections.abc import Awaitable
from asyncio.futures import Future

HAVE_FUTURE = 'asyncio'
_HaystackOperationBase = Awaitable
except ImportError:
HAVE_FUTURE = None
_HaystackOperationBase = object

if HAVE_FUTURE is None:
# Try Tornado
try:
from tornado.concurrent import Future
HAVE_FUTURE = 'tornado'
except ImportError:
pass


class NotReadyError(Exception):
"""
Exception raised when an attempt is made to retrieve the result of an
operation before it is ready.
"""

pass


class HaystackOperation(_HaystackOperationBase):
"""
A core state machine object. This implements the basic interface presented
for all operations in pyhaystack.
"""

def __init__(self, result_copy=True, result_deepcopy=True):
"""
Initialisation. This should be overridden by subclasses to accept and
validate the inputs presented for the operation, raising an appropriate
Exception subclass if the inputs are found to be invalid.
These should be stored here by the initialisation function as private
variables in suitably sanitised form. The core state machine object
shall then be created and stored before the object is returned to the
caller.
"""
# Event object to represent when this operation is "done"
self._done_evt = Event()

# Signal emitted when the operation is "done"
self.done_sig = Signal(name="done", threadsafe=True)

# Result returned by operation
self._result = None
self._result_copy = result_copy
self._result_deepcopy = result_deepcopy

def go(self):
"""
Start processing the operation. This is called by the caller (so after
all __init__ functions have executed) in order to begin the asynchronous
operation.
"""
# This needs to be implemented in the subclass.
raise NotImplementedError(
"To be implemented in subclass %s" % self.__class__.__name__
)
assert NotReadyError
assert BaseHaystackOperation

def wait(self, timeout=None):
"""
Wait for an operation to finish. This should *NOT* be called in the
same thread as the thread executing the operation as this will
deadlock.
"""
self._done_evt.wait(timeout)

if HAVE_FUTURE == 'asyncio':
def __await__(self):
"""
Return a future object which can be awaited by asyncio-aware
tools like ipython and in asynchronous scripts.
"""
res = yield from self.future
return res

@property
def future(self):
"""
Return a Future object (asyncio or Tornado).
"""
if HAVE_FUTURE is None:
raise NotImplementedError(
'Futures require either asyncio and/or Tornado to work'
)

# Both Tornado and asyncio future classes work the same.
future = Future()
if self.is_done:
self._set_future(future)
else:
# Not done yet, wait for it
def _on_done(*a, **kwa):
self._set_future(future)
self.done_sig.connect(_on_done)

# Return the future for the caller
return future

@property
def state(self):
"""
Return the current state machine's state.
"""
return self._state_machine.current

@property
def is_done(self):
"""
Return true if the operation is complete.
"""
return self._state_machine.is_finished()

@property
def is_failed(self):
"""
Return true if the result is an Exception.
"""
return isinstance(self._result, AsynchronousException)

@property
def result(self):
"""
Return the result of the operation or raise its exception.
Raises NotReadyError if not ready.
"""
if not self.is_done:
raise NotReadyError()

if self.is_failed:
self._result.reraise()

if not self._result_copy:
# Return the original instance (do not copy)
return self._result
elif self._result_deepcopy:
# Return a deep copy
return deepcopy(self._result)
else:
# Return a shallow copy
return self._result.copy()

def __repr__(self):
"""
Return a representation of this object's state.
"""
if self.is_failed:
return "<%s failed>" % self.__class__.__name__
elif self.is_done:
return "<%s done: %s>" % (self.__class__.__name__, self._result)
else:
return "<%s %s>" % (self.__class__.__name__, self.state)

def _done(self, result):
"""
Return the result of the operation to any listeners.
"""
self._result = result
self._done_evt.set()
self.done_sig.emit(operation=self)

def _set_future(self, future):
"""
Set the given future to the operation result, if known
or raise an exception otherwise.
"""
# It's already done
try:
future.set_result(self.result)
except Exception as e:
future.set_exception(e)
if HAVE_FUTURE == 'asyncio':
from .awaitableop import HaystackOperation
else:
class HaystackOperation(BaseHaystackOperation):
pass

0 comments on commit faa5f91

Please sign in to comment.