# Copyright 2011-2014 Biomedical Imaging Group Rotterdam, Departments of
# Medical Informatics and Radiology, Erasmus MC, Rotterdam, The Netherlands
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains both the Updateable class and UpdateableMeta meta-class
for objects which support updates within the fastr system
"""
from abc import ABCMeta, abstractmethod
import os
import types
import threading
from .. import exceptions
from ..helpers import log
class UpdateableMeta(ABCMeta):
"""
A metaclass for objects which are updateable and need some methods/properties
to trigger an update.
"""
@classmethod
def calcmro(mcs, bases):
"""Calculate the Method Resolution Order of bases using the C3 algorithm.
Suppose you intended creating a class K with the given base classes. This
function returns the MRO which K would have, *excluding* K itself (since
it doesn't yet exist), as if you had actually created the class.
Another way of looking at this, if you pass a single class K, this will
return the linearization of K (the MRO of K, *including* itself).
:param bases: the list of bases for which create the MRO
:return: the list representing the entire MRO, except the (non-existing) class itself
Note: Taken from http://code.activestate.com/recipes/577748-calculate-the-mro-of-a-class/
Created by Steven D'Aprano and licensed under the MIT license
"""
seqs = [list(C.__mro__) for C in bases] + [list(bases)]
res = []
while True:
non_empty = [item for item in seqs if item]
if not non_empty:
# Nothing left to process, we're done.
return tuple(res)
for seq in non_empty: # Find merge candidates among seq heads.
candidate = seq[0]
not_head = [s for s in non_empty if candidate in s[1:]]
if not_head:
# Reject the candidate.
candidate = None
else:
break
if not candidate:
raise TypeError("inconsistent hierarchy, no C3 MRO is possible")
res.append(candidate)
for seq in non_empty:
# Remove candidate.
if seq[0] == candidate:
del seq[0]
@classmethod
def find_member(mcs, name, parents, dct):
"""
Find a member of the class in the same way as Python would if it had a given dict and set of bases
:param mcs: metaclass at work
:param name: name of the class to be created
:param parents: list of the bases for the new class
:param dct: the dict of the class being created
:return: the firstly resolved member or None if nothing found
"""
if name in dct:
return dct[name]
for cls in mcs.calcmro(parents):
if hasattr(cls, name):
return getattr(cls, name)
return None
def __new__(mcs, name, parents, dct):
if '__updatetriggers__' in dct:
triggers = dct['__updatetriggers__']
for trigger in triggers:
fnc = mcs.find_member(trigger, parents, dct)
if fnc is not None:
if isinstance(fnc, (types.FunctionType, types.MethodType, types.BuiltinMethodType)):
log.debug('Adding update trigger {} to {}'.format(trigger, name))
dct[trigger] = mcs.updatetrigger(fnc)
else:
log.debug('Skipping trigger {} for {} (wrong type: {})'.format(trigger, name, type(fnc).__name__))
else:
log.debug('Skipping trigger {} for {} (not in dct)'.format(trigger, name))
raise ValueError()
return super(UpdateableMeta, mcs).__new__(mcs, name, parents, dct)
@staticmethod
def updatetrigger(fnc):
"""
Function decorator to make a function trigger an update after being
called. This is a way to easily have function trigger an update
after setting a value without writing tons of wrapper functions.
The function keeps the original docstring and appends a note to it.
"""
def wrapper(self, *args, **kwargs):
"""
Decorator wrapper around a function. This docstring will be
changed for each wrapped function
"""
# First call the wrapped function
fnc(self, *args, **kwargs)
# Call the update function
# This function name is chosen so that it has low chance of conflicts
if self.__updating__:
self.__updatefunc__()
# Add a note to the docstring indicating what is going on
original = '{}.{}'.format(fnc.__module__, fnc.__name__)
docstring = fnc.__doc__.lstrip('\n')
indent = docstring[:len(docstring) - len(docstring.lstrip())]
extra_doc = ('\n.. note::\n'
' This is a wrapped version of ``{orig}``\n'
' which triggers an update of the object after\n'
' being called').format(orig=original)
extra_doc = '\n{}'.format(indent).join(extra_doc.splitlines())
wrapper.__doc__ = docstring.rstrip() + "\n{indent}\n{indent}{extra_doc}\n{indent}".format(indent=indent, extra_doc=extra_doc)
return wrapper
class Updateable(object, metaclass=UpdateableMeta):
"""
Super class for all classes that can be updated and have a status.
These objects can be valid/invalid state. These states are set by
the function update. This allows for interactively checking the network.
"""
#: Which methods need to be wrapped to trigger an update. Override this
#: value to have the functions automatically wrapped. E.g.
#: ``__update_triggers__ = ['append', 'insert', '__setitem__']`` to have
#: these functions wrapped.
__updatetriggers__ = []
#: Flag to indicate that this object is allowed to update
__updating__ = True
#: Lock to avoid multiple updates happening at the same time
__updateinprogress__ = threading.Lock()
def __init__(self):
"""
Constructor, creates the status field
:return: newly created object
"""
self._status = {'key': None, 'valid': None, 'messages': []}
def __getstate__(self):
"""
Retrieve the state of the object, make sure the status is not part of
the description as it will not be valid after re-creating the object.
:return: the state of the object
:rtype dict:
"""
return {}
def __setstate__(self, state):
"""
Set the state of the object by the given state. This adds a clean
status field, making sure it is not unintended, outdated information
from before serialization.
:param dict state: The state to populate the object with
"""
self._status = {'key': None, 'valid': None, 'messages': []}
@property
def messages(self):
"""
The messages of the last update
"""
return self._status['messages']
@property
def valid(self):
"""
Flag indicating that the object is valid
"""
return self._status['valid']
def update(self, key=None, forward=True, backward=False):
"""
Default function for updating, it can be called without key to have a
new update started with a new key.
:param int key: a key for this update, should be different than the
last update key
:param bool forward: flag indicating to update forward in the network
:param bool backward: flag indicating to update backward in the network
"""
# If updating is disable, we don't do anything
if not self.__updating__:
return
# If this is the first update function called in this round, create a key
lock = False
if key is None:
key = hash(os.urandom(128))
# Block update until previous updates are finished
log.debug('Getting update lock')
lock = Updateable.__updateinprogress__.acquire()
log.debug('Got update lock')
# Only update if not update in this update round
if key == self._status['key']:
# Release updatable lock, this should never be needed, but if not done when needed causes a deadlock
if lock:
log.warning('Releasing lock in unexpected place! Might be a bug!')
Updateable.__updateinprogress__.release()
log.debug('Released update lock')
return
# Store key to mark last update round
self._status['key'] = key
# Run the update func
self._update(key, forward, backward)
# Release updatable lock
if lock:
Updateable.__updateinprogress__.release()
log.debug('Released update lock')
return key
# Set the update function for the trigger functions to call
__updatefunc__ = update
@abstractmethod
def _update(self, key, forward=True, backward=False):
"""
The actual update function to be used. This is an abstract function
that Updateable objects must implement.
:param int key: a key for this update, should be different than the
last update key
:param bool forward: flag indicating to update forward in the network
:param bool backward: flag indicating to update backward in the network
"""
raise exceptions.FastrNotImplementedError('Purposefully not implemented')