__author__ = "Andre Merzky, Ole Weidner"
__copyright__ = "Copyright 2012-2013, The SAGA Project"
__license__ = "MIT"
""" Attribute interface """
import radical.utils as ru
import radical.utils.signatures as rus
from . import exceptions as se
# ------------------------------------------------------------------------------
import datetime
import datetime
import traceback
import inspect
import string
import copy
import re
from pprint import pprint
# FIXME: add a tagging 'Monitorable' interface, which enables callbacks.
now = datetime.datetime.now
never = datetime.datetime.min
# ------------------------------------------------------------------------------
#
# define a couple of constants for the attribute API, mostly for registering
# attributes.
#
# type enums
ANY = 'any' # any python type can be set
URL = 'url' # URL type (string + URL parser checks)
INT = 'int' # Integer type
FLOAT = 'float' # float type
STRING = 'string' # string, duh!
BOOL = 'bool' # True or False or Maybe
ENUM = 'enum' # value is any one of a list of candidates
TIME = 'time' # seconds since epoch, or any py time thing
# which can be converted into such
# FIXME: conversion not implemented
# mode enums
WRITEABLE = 'writeable' # the consumer of the interface can change
# the attrib value
READONLY = 'readonly' # the consumer of the interface can not
# change the attrib value. The
# implementation can still change it.
FINAL = 'final' # neither consumer nor implementation can
# change the value anymore
ALIAS = 'alias' # variable is deprecated, and alias'ed to
# a different variable.
# attrib extensions
EXTENDED = 'extended' # attribute added as extension
PRIVATE = 'private' # attribute added as private
# flavor enums
SCALAR = 'scalar' # the attribute value is a single data element
DICT = 'dict' # the attribute value is a dict of data elements
VECTOR = 'vector' # the attribute value is a list of data elements
# ------------------------------------------------------------------------------
#
# Callback (Abstract) Class
#
[docs]class Callback () :
"""
Callback base class.
All objects using the Attribute Interface allow to register a callback for
any changes of its attributes, such as 'state' and 'state_detail'. Those
callbacks can be python call'ables, or derivates of this callback base
class. Instances which inherit this base class MUST implement (overload)
the cb() method.
The callable, or the callback's cb() method is what is invoked whenever the
SAGA implementation is notified of an change on the monitored object's
attribute.
The cb instance receives three parameters upon invocation:
- obj: the watched object instance
- key: the watched attribute (e.g. 'state' or 'state_detail')
- val: the new value of the watched attribute
If the callback returns 'True', it will remain registered after invocation,
to monitor the attribute for the next subsequent state change. On returning
'False' (or nothing), the callback will not be called again.
To register a callback on a object instance, use::
class MyCallback (saga.Callback):
def __init__ (self):
pass
def cb (self, obj, key, val) :
print(" %s\\n %s (%s) : %s" % self._msg, obj, key, val)
jd = saga.job.Description ()
jd.executable = "/bin/date"
js = saga.job.Service ("fork://localhost/")
job = js.create_job(jd)
cb = MyCallback()
job.add_callback(saga.STATE, cb)
job.run()
See documentation of the :class:`saga.Attribute` interface for further
details and examples.
"""
def __call__ (self, obj, key, val) :
return self.cb (obj, key, val)
[docs] def cb (self, obj, key, val) :
""" This is the method that needs to be implemented by the application
Keyword arguments::
obj: the watched object instance
key: the watched attribute
val: the new value of the watched attribute
Return::
keep: bool, signals to keep (True) or remove (False) the callback
after invocation
Callback invocation MAY (and in general will) happen in a separate
thread -- so the application need to make sure that the callback
code is thread-safe.
The boolean return value is used to signal if the callback should
continue to listen for events (return True) , or if it rather should
get unregistered after this invocation (return False).
"""
pass
# ------------------------------------------------------------------------------
#
class _AttributesBase (object) :
"""
This class only exists to host properties -- as object itself does *not* have
properties! This class is not part of the public attribute API.
"""
# --------------------------------------------------------------------------
#
@rus.takes ('_AttributesBase')
@rus.returns (rus.nothing)
def __init__ (self) :
pass
# ------------------------------------------------------------------------------
#
[docs]class Attributes (_AttributesBase, ru.DictMixin) :
"""
Attribute Interface Class
The Attributes interface implements the attribute semantics of the SAGA Core
API specification (http://ogf.org/documents/GFD.90.pdf). Additionally, this
implementation provides that semantics the python property interface. Note
that a *single* set of attributes is internally managed, no matter what
interface is used for access.
A class which uses this interface can internally specify which attributes
can be set, and what type they have. Also, default values can be specified,
and the class provides a rudimentary support for converting scalar
attributes into vector attributes and back.
Also, the consumer of this API can register callbacks, which get triggered
on changes to specific attribute values.
Example use case::
# --------------------------------------------------------------------------------
class Transliterator ( saga.Attributes ) :
def __init__ (self, *args, **kwargs) :
# setting attribs to non-extensible will cause the cal to init below to
# complain if attributes are specified. Default is extensible.
# self._attributes_extensible (False)
# pass args to base class init (requires 'extensible')
super (Transliterator, self).__init__ (*args, **kwargs)
# setup class attribs
self._attributes_register ('apple', 'Appel', URL, SCALAR, WRITEABLE)
self._attributes_register ('plum', 'Pruim', STRING, SCALAR, READONLY)
# setting attribs to non-extensible at *this* point will have allowed
# custom user attribs on __init__ time (via args), but will then forbid
# any additional custom attributes.
# self._attributes_extensible (False)
# --------------------------------------------------------------------------------
if __name__ == "__main__":
# define a callback method. This callback can get registered for
# attribute changes later.
# ----------------------------------------------------------------------------
def cb (key, val, obj) :
# the callback gets information about what attribute was changed
# on what object:
print("called: %s - %s - %s" % (key, str(val), type (obj)))
# returning True will keep the callback registered for further
# attribute changes.
return True
# ----------------------------------------------------------------------------
# create a class instance and add a 'cherry' attribute/value on
# creation.
trans = Transliterator (cherry='Kersche')
# use the property interface to mess with the pre-defined
# 'apple' attribute
print("\\n -- apple")
print(trans.apple)
trans.apple = 'Abbel'
print(trans.apple)
# add our callback to the apple attribute, and trigger some changes.
# Note that the callback is also triggered when the attribute's
# value changes w/o user control, e.g. by some internal state
# changes.
trans.add_callback ('apple', cb)
trans.apple = 'Apfel'
# Setting an attribute final is actually an internal method, used by
# the implementation to signal that no further changes on that
# attribute are expected. We use that here for demonstrating the
# concept though. Callback is invoked on set_final().
trans._attributes_set_final ('apple')
trans.apple = 'Abbel'
print(trans.apple)
# mess around with the 'plum' attribute, which was marked as
# ReadOnly on registration time.
print("\\n -- plum")
print(trans.plum)
# trans.plum = 'Pflaume' # raises readonly exception
# trans['plum'] = 'Pflaume' # raises readonly exception
print(trans.plum)
# check if the 'cherry' attribute exists, which got created on
# instantiation time.
print("\\n -- cherry")
print(trans.cherry)
# as we have 'extensible' set, we can add a attribute on the fly,
# via either the property interface, or via the GFD.90 API of
# course.
print("\\n -- peach")
print(trans.peach)
trans.peach = 'Birne'
print(trans.peach)
This example will result in::
-- apple
Appel
Appel
Abbel
called: apple - Abbel Appel - <class '__main__.Transliterator'>
called: apple - Apfel - <class '__main__.Transliterator'>
called: apple - Apfel - <class '__main__.Transliterator'>
Apfel
-- plum
Pruim
Pruim
-- cherry
Kersche
-- peach
Berne
Birne
"""
# internally used constants to distinguish API from adaptor calls
_UP = '_up'
_DOWN = '_down'
# two regexes for converting CamelCase into under_score_casing, as static
# class vars to avoid frequent recompilation
_camel_case_regex_1 = re.compile('(.)([A-Z][a-z]+)')
_camel_case_regex_2 = re.compile('([a-z0-9])([A-Z])')
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
rus.anything,
rus.anything)
@rus.returns (rus.nothing)
def __init__ (self, *args, **kwargs) :
"""
This method is not supposed to be directly called by the consumer of
this API -- it should be called via derived object construction.
_attributes_t_init makes sure that the basic structures are in place on
the attribute dictionary - this saves us ton of safety checks later on.
"""
# initialize state
d = self._attributes_t_init ()
# call to update and the args/kwargs handling seems to be part of the
# dict interface conventions *shrug*
# we use similar mechanism to initialize attribs here:
for arg in args :
if arg == None:
# be resiliant to empty initialization
pass
elif isinstance (arg, dict):
d['extensible'] = True # it is just being extended ;)
d['camelcasing'] = True # default for dict inits
for key in list(arg.keys()):
us_key = self._attributes_t_underscore(key)
self._attributes_i_set(us_key, arg[key], force=True, flow=self._UP)
else:
raise se.BadParameter("initialization expects dictionary")
for key in list(kwargs.keys ()) :
self.set_attribute (key, kwargs[key])
# make iterable
d['_iterpos'] = 0
self.list_attributes ()
# --------------------------------------------------------------------------
#
# Internal interface tools.
#
# These tools are only for internal use, and should never be called from
# outside of this module.
#
# Naming: _attributes_t_*
#
@rus.takes ('Attributes',
rus.optional (str))
@rus.returns (dict)
def _attributes_t_init (self, key=None) :
"""
This internal function is not to be used by the consumer of this API.
The _attributes_t_init method initializes the interface's internal data
structures. We always need the attribute dict, and the extensible flag.
Everything else can be added on the fly. The method will not overwrite
any settings -- initialization occurs only once!
If a key is given, the existence of this key is checked. An exception
is raised if the key does not exist.
The internal data are stored as property on the _AttributesBase class.
Storing them as property on *this* class would obviously result in
recursion...
"""
d = {}
try :
d = _AttributesBase.__getattribute__ (self, '_d')
except :
# need to initialize -- any exceptions in the code below should fall through
d['attributes'] = {}
d['extensible'] = True
d['private'] = True
d['camelcasing'] = False
d['getter'] = None
d['setter'] = None
d['lister'] = None
d['caller'] = None
d['recursion'] = False
d['_iterpos'] = 0
_AttributesBase.__setattr__ (self, '_d', d)
# check if we know about the given attribute
if key :
if key not in d['attributes'] :
raise se.DoesNotExist ("attribute key is invalid: %s" % (key))
# all is well
return d
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str)
@rus.returns (str)
def _attributes_t_keycheck (self, key) :
"""
This internal function is not to be used by the consumer of this API.
For the given key, check if the key name is valid, and/or if it is
aliased.
If the does not yet exist, the validity check is performed, and allows
to limit dynamically added attribute names (for 'extensible' sets).
If the key does exist, the alias check triggers a deprecation warning,
and returns the aliased key for transparent operation.
"""
# make sure interface is ready to use
d = self._attributes_t_init ()
# perform name validity checks if key is new
if not key in d['attributes'] :
# FIXME: we actually don't have any tests, yet. We should allow to
# configure such via, say, _attributes_add_check (callable (key))
pass
# if key is known, check for aliasing
else:
# check if we know about the given attribute
if d['attributes'][key]['mode'] == ALIAS :
alias = d['attributes'][key]['alias']
print("attribute '%s' is deprecated - use '%s'" % (key, alias))
key = alias
return key
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.anything)
@rus.returns (rus.nothing)
def _attributes_t_call_cb (self, key, val) :
"""
This internal function is not to be used by the consumer of this API.
It triggers the invocation of all callbacks for a given attribute.
Callbacks returning False (or nothing at all) will be unregistered after
their invocation.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
# avoid recursion
if d['attributes'][key]['recursion'] :
return
callbacks = d['attributes'][key]['callbacks']
# iterate over a copy of the callback list, so that remove does not
# screw up the iteration
for cb in list (callbacks) :
call = cb
# got the callable - call it!
# raise and lower recursion shield as needed
ret = False
try :
d['attributes'][key]['recursion'] = True
ret = call (self, key, val)
finally :
d['attributes'][key]['recursion'] = False
# remove callbacks which return 'False', or raised and exception
if not ret :
callbacks.remove (cb)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.anything)
@rus.returns (rus.nothing)
def _attributes_t_call_setter (self, key, val) :
"""
This internal function is not to be used by the consumer of this API.
It triggers the setter callbacks, to signal that the attribute value
has just been set and should be propagated as needed.
"""
# make sure interface is ready to use.
d = self._attributes_t_init (key)
# avoid recursion
if d['attributes'][key]['recursion'] :
return
# no callbacks for private keys
if key[0] == '_' and d['private'] :
return
# key_setter overwrites results from all_setter
all_setter = d['setter']
key_setter = d['attributes'][key]['setter']
# Get the value via the attribute setter. The setter will not call
# attrib setters or callbacks, due to the recursion guard.
# Set the value via the native setter (to the backend),
# always raise and lower the recursion shield
#
# If both are present, we can ignore *one* exception. If one
# is present, exceptions are not ignored.
#
# always raise and lower the recursion shield.
can_ignore = 0
if all_setter and key_setter : can_ignore = 1
if all_setter :
try :
d['attributes'][key]['recursion'] = True
all_setter (key, val)
except Exception as e :
# ignoring failures from setter
pass
except Exception as e :
can_ignore -= 1
if not can_ignore : raise e
finally :
d['attributes'][key]['recursion'] = False
if key_setter :
try :
d['attributes'][key]['recursion'] = True
key_setter (val)
except:
can_ignore -= 1
if not can_ignore : raise
finally :
d['attributes'][key]['recursion'] = False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str)
@rus.returns (rus.nothing)
def _attributes_t_call_getter (self, key) :
"""
This internal function is not to be used by the consumer of this API.
It triggers the getter callbacks, to signal that the attribute value
is about to be accesses and should be updated as needed.
"""
# make sure interface is ready to use.
d = self._attributes_t_init (key)
# avoid recursion
if d['attributes'][key]['recursion'] :
return
# no callbacks for private keys
if key[0] == '_' and d['private'] :
return
# key getter overwrites results from all_getter
all_getter = d['getter']
key_getter = d['attributes'][key]['getter']
# # Note that attributes have a time-to-live (ttl). If a _attributes_i_get
# # operation is attempted within 'time-of-last-update + ttl', the operation
# # is not triggering backend getter hooks, to avoid trashing (hooks are
# # expected to be costly). The force flag set to True will request to call
# # registered getter hooks even if ttl is not yet expired.
#
# # For example, job.wait() will update the plugin level state to 'Done',
# # but the cached job.state attribute will remain 'New' as the plugin does
# # not push the state change upward
#
# age = self._attributes_t_get_age (key)
# ttl = d['attributes'][key]['ttl']
#
# if age < ttl :
# return
# get the value from the native getter (from the backend), and
# get it via the attribute getter. The getter will not call
# attrib setters or callbacks, due to the recursion guard.
#
# If both are present, we can ignore *one* exception. If one
# is present, exceptions are not ignored.
#
# always raise and lower the recursion shield.
retries = 1
if all_getter and key_getter : retries = 2
if all_getter :
try :
d['attributes'][key]['recursion'] = True
val=all_getter (key)
d['attributes'][key]['value'] = val
except Exception:
retries -= 1
if not retries : raise
finally :
d['attributes'][key]['recursion'] = False
if key_getter :
try :
d['attributes'][key]['recursion'] = True
val=key_getter ()
d['attributes'][key]['value'] = val
except Exception:
retries -= 1
if not retries : raise
finally :
d['attributes'][key]['recursion'] = False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str)
@rus.returns (rus.list_of (str))
def _attributes_t_call_lister (self) :
"""
This internal function is not to be used by the consumer of this API.
It triggers the lister callback, to signal that the attribute list
is about to be accesses and should be updated as needed.
"""
# make sure interface is ready to use.
d = self._attributes_t_init ()
# avoid recursion
if d['recursion'] :
return
lister = d['lister']
if lister :
# the lister is simply called, and it is expected that it internally
# adds/removes attributes as needed.
#
# always raise and lower the recursion shield
try :
d['recursion'] = True
lister ()
finally :
d['recursion'] = False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
int,
callable)
@rus.returns (rus.anything)
def _attributes_t_call_caller (self, key, id, cb) :
"""
This internal function is not to be used by the consumer of this API.
It triggers the invocation of any registered caller function, usually
after an 'add_callback()' call.
"""
# make sure interface is ready to use.
d = self._attributes_t_init (key)
# avoid recursion
if d['recursion'] :
return
# no callbacks for private keys
if key[0] == '_' and d['private'] :
return
caller = d['caller']
if caller :
# the caller is simply called, and it is expected that it internally
# adds/removes callbacks as needed
#
# always raise and lower the recursion shield
try :
d['recursion'] = True
return caller (key, id, cb)
finally :
d['recursion'] = False
return
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str)
@rus.returns (str)
def _attributes_t_underscore (self, key, force=False) :
"""
This internal function is not to be used by the consumer of this API.
The method accepts a CamelCased word, and translates that into
'under_score' notation -- IFF 'camelcasing' is set
Kudos: http://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-camel-case
"""
# make sure interface is ready to use
d = self._attributes_t_init ()
if force or d['camelcasing'] :
temp = Attributes._camel_case_regex_1.sub(r'\1_\2', key)
return Attributes._camel_case_regex_2.sub(r'\1_\2', temp).lower()
else :
return key
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str)
@rus.returns (rus.anything)
def _attributes_t_conversion (self, key, val) :
"""
This internal function is not to be used by the consumer of this API.
The method checks a given attribute value against the attribute's
flags, and performs some simple type conversion as needed. Also, the
method will restore a 'None' value to the attribute's default value.
A deriving class can add additional value checks for attributes by
calling :func:`_attributes_add_check` (key, check).
"""
# make sure interface is ready to use. We do not check for keys, that
# needs to be done in the calling method. For example, on 'set', type
# conversions will be performed, but the key will not exist previously.
d = self._attributes_t_init ()
# if the key is not known
if not key in d['attributes'] :
# cannot handle unknown attributes. Attributes which have been
# registered earlier will be fine, as they have type information.
return val
# check if a value is given. If not, revert to the default value
# (if available)
if val == None :
if 'default' in d['attributes'][key] :
val = d['attributes'][key]['default']
# perform flavor and type conversion
val = self._attributes_t_conversion_flavor (key, val)
# apply all value checks on the conversion result
for check in d['attributes'][key]['checks'] :
ret = check (key, val)
if ret != True :
raise se.BadParameter ("attribute value %s is not valid: %s" % (key, ret))
# aaaand done
return val
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.anything)
@rus.returns (rus.anything)
def _attributes_t_conversion_flavor (self, key, val) :
"""
This internal function is not to be used by the consumer of this API.
This method should ONLY be called by _attributes_t_conversion!
"""
# FIXME: there are possibly nicer and more reversible ways to
# convert the flavors...
# easiest conversion of them all... ;-)
if val == None :
return None
# make sure interface is ready to use.
d = self._attributes_t_init (key)
# check if we need to serialize a list into a scalar
f = d['attributes'][key]['flavor']
t = d['attributes'][key]['type']
if f == ANY :
# leave it alone
return val
elif f == VECTOR :
# we want a vector
if isinstance (val, list) :
# val is already vec - apply type conversion on all elems
ret = []
for elem in val :
ret.append (self._attributes_t_conversion_type (key, elem))
return ret
else :
# need to create vec from scalar
if isinstance (val, str) :
# for string values, we split on white spaces and type-convert
# all elements
vec = val.split ()
ret = []
for element in vec :
ret.append (self._attributes_t_conversion_type (key, element))
return ret
else :
# all non-string types are interpreted as only element of
# a single-member list
return [self._attributes_t_conversion_type (key, val)]
elif f == DICT :
# we want a dict
if isinstance (val, dict) :
# done :-)
return val
if isinstance (val, list) :
# if target type is a dict, we parse the values and
# split on '=', creating the dict. That will only work for
# string typed values
out = {}
for elem in val :
(key, val) = str(elem).split ('=', 1)
out[key] = val
return out
if isinstance (val, str) :
# we assume a colon or comma separated list of = separated
# key/value pairs
elems = val.split (':')
out = {}
if len(elems) == 1 :
elems = val.split (',')
for elem in elems :
(key, val) = str(elem).strip ().split ('=', 1)
out[key] = val
return out
# can't handle any other types...
elif f == SCALAR :
# we want a scalar
if t == ANY :
# no need to do anything, really
return val
if isinstance (val, list) :
# need to create scalar from vec
if len (val) > 1 :
# if the list has more than one element, we use an intermediate
# string representation of the list before converting to a scalar
# This is the weakest conversion mode, and will not very
# likely yield useful results.
tmp = ""
for i in val :
tmp += str(i) + " "
return self._attributes_t_conversion_type (key, tmp)
elif len (val) == 1 :
# for single element lists, we simply use the one element as
# scalar value
return self._attributes_t_conversion_type (key, val[0])
else :
# no value in list
return None
else :
# scalar is already scalar, just do type conversion
return self._attributes_t_conversion_type (key, val)
# we should never get here...
raise se.BadParameter ("Cannot evaluate attribute flavor (%s) : %s" % (key, str(f)))
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.anything)
@rus.returns (rus.anything)
def _attributes_t_conversion_type (self, key, val) :
"""
This internal function is not to be used by the consumer of this API.
This method should ONLY be called by _attributes_t_conversion!
"""
# make sure interface is ready to use.
d = self._attributes_t_init (key)
# oh python, how about a decent switch statement???
t = d['attributes'][key]['type']
ret = None
try :
# FIXME: add time/date conversion to/from string
if t == ANY : return val
elif t == INT : return int (val)
elif t == FLOAT : return float (val)
elif t == BOOL : return bool (val)
elif t == STRING : return str (val)
else : return val
except ValueError as e:
raise se.BadParameter ("attribute value %s has incorrect type: %s" % (key, val)) \
from e
# we should never get here...
raise se.BadParameter ("Cannot evaluate attribute type (%s) : %s" % (key, str(t)))
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str)
@rus.returns (str)
def _attributes_t_wildcard2regex (self, pattern) :
"""
This internal function is not to be used by the consumer of this API.
This method converts a string containing POSIX shell wildcards into
a regular expression with the same matching properties::
* -> .*
? -> .
{a,b,c} -> (a|b|c)
[abc] -> [abc]
[!abc] -> [^abc]
"""
re = pattern
re.replace ('*', '.*') # set of characters
re.replace ('?', '.' ) # single character
# character classes
match = re.find ('[', 0)
while match >= 0 :
if re[match + 1] == '!' :
re[match + 1] = '^'
match = re.find ('[', match + 1)
# find opening { and closing }
first = re.find ('{', 0)
last = re.find ('}', first + 1)
# while match
while first >= 0 and last >= 0 :
# replace with ()
re[first] = '('
re[last] = '('
# also, replace all ',' with with '|' for alternatives
comma = re.find (',', first)
while comma >= 0 :
re[comma] = '|'
comma = re.find (',', comma + 1)
# done - find next bracket pair...
first = re.find ('{', last + 1)
last = re.find ('}', first + 1)
return re
# --------------------------------------------------------------------------
#
def _attributes_t_get_age (self, key) :
""" get the age of the attribute, i.e. seconds.microseconds since last set """
# make sure interface is ready to use.
d = self._attributes_t_init (key)
last = d['attributes'][key]['last']
age = now() - last
return (age.microseconds + (age.seconds + age.days * 24 * 3600) * 1e6) / 1e6
# --------------------------------------------------------------------------
#
# internal interface
#
# This internal interface is used by the public interfaces (dict,
# properties, GFD.90). We assume that CamelCasing and under_scoring is
# sorted out before this internal interface is called. All other tests,
# verifications, and conversion are done here though.
#
# Naming: _attributes_i_*
#
def _attributes_i_set (self, key, val=None, force=False, flow=_DOWN) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
See :func:`set_attribute` (key, val) for details.
New value checks can be added dynamically, and per attribute, by calling
:func:`_attributes_add_check` (key, callable).
Some internal methods can set the 'force' flag, and will be able to set
attributes even in ReadOnly mode. That is, for example, used for getter
hooks. Note that the Final flag will be honored even if Force is set,
and will result in the set request being ignored.
"""
# make sure interface is ready to use
d = self._attributes_t_init ()
# if the key is not known
if not key in d['attributes'] :
if key[0] == '_' and d['private'] :
# if the set is private, we can register the new key. It
# won't have any callbacks at this point.
self._attributes_register (key, None, ANY, ANY, WRITEABLE, EXTENDED, flow=flow)
elif flow==self._UP or d['extensible'] :
# if the set is extensible, we can register the new key. It
# won't have any callbacks at this point.
self._attributes_register (key, None, ANY, ANY, WRITEABLE, EXTENDED, flow=flow)
elif force :
# someone *really* wants this attrib to be set...
self._attributes_register (key, None, ANY, SCALAR, WRITEABLE, EXTENDED, flow=flow)
else :
# we cannot add new keys on non-extensible / non-private sets
raise se.IncorrectState ("attribute set is not extensible/private (key %s)" % key)
# known attribute
else :
# check if we are allowed to change the attribute - complain if not.
# Also, simply ignore write attempts to finalized keys.
if 'mode' in d['attributes'][key] :
mode = d['attributes'][key]['mode']
if FINAL == mode :
return
elif READONLY == mode :
if not force :
raise se.BadParameter ("attribute %s is not writeable" % key)
# permissions are confirmed, set the attribute with conversion etc.
# NOTE: keep the original value around for the setter
orig_val = val
# apply any attribute conversion
val = self._attributes_t_conversion (key, val)
# make sure the key's value entry exists
if not 'value' in d['attributes'][key] :
d['attributes'][key]['value'] = None
# only once an attribute is explicitly set, it 'exists' for the purpose
# of the 'attribute_exists' call, and the key iteration
d['attributes'][key]['exists'] = True
# # only actually change the attribute when the new value differs --
# # and only then invoke any callbacks and hooked setters
# if val != d['attributes'][key]['value'] :
#
# NOTE: this check is disabled now: we certainly want to update 'last',
# and IMHO that should also imply a notification call, etc. FWIW, the
# spec is inconclusive here.
#
# if val != d['attributes'][key]['value'] :
d['attributes'][key]['value'] = val
d['attributes'][key]['last'] = now ()
if flow==self._DOWN :
# NOTE: we use the orig_val here, to make the environment hooks
# happy which we introduced for BJ backward compatibility (FIXME)
self._attributes_t_call_setter (key, orig_val)
self._attributes_t_call_cb (key, val)
# --------------------------------------------------------------------------
#
def _attributes_i_get (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`get_attribute` (key) for details.
Note that this method is not performing any checks or conversions --
those are all performed when *setting* an attribute. So, any attribute
flags (type, mode, flavor) are evaluated on setting, not on getting.
This implementation does not account for resulting race conditions
(changing attribute types after setting for example) -- but the public
API does not allow that anyway.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
if flow == self._DOWN :
self._attributes_t_call_getter (key)
if 'value' in d['attributes'][key] :
return d['attributes'][key]['value']
if 'default' in d['attributes'][key] :
return d['attributes'][key]['default']
return None
# --------------------------------------------------------------------------
#
def _attributes_i_list (self, ext=True, priv=False, CamelCase=True, flow=_DOWN) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`list_attributes` () for details.
Note that registration alone does not qualify for listing. If 'ext' is
True (default),extended attributes are listed, too.
"""
# make sure interface is ready to use
d = self._attributes_t_init ()
# call list hooks to update state for listing
self._attributes_t_call_lister ()
ret = []
for key in sorted(d['attributes'].keys()) :
if d['attributes'][key]['mode'] != ALIAS :
if d['attributes'][key]['exists'] :
e = d['attributes'][key]['extended']
p = d['attributes'][key]['private']
k = key
if CamelCase :
k = d['attributes'][key]['camelcase']
if e and ext :
if p and priv :
ret.append (k)
elif not p :
ret.append (k)
elif not e :
if p and priv :
ret.append (k)
elif not p :
ret.append (k)
return ret
# --------------------------------------------------------------------------
#
def _attributes_i_find (self, pattern, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`find_attributes` (pattern) for details.
"""
# FIXME: wildcard-to-regex
# make sure interface is ready to use
d = self._attributes_t_init ()
# separate key and value pattern
p_key = "" # string pattern
p_val = "" # string pattern
pc_key = None # compiled pattern
pc_val = None # compiled pattern
if pattern[0] == '=' :
# no key pattern present, only grep on values
p_val = self._attributes_t_wildcard2regex (pattern[1:])
else :
p = re.compile (r'[^\]=')
tmp = p.split (pattern, 2) # only split on first '='
if len (tmp) > 0 :
# at least one elem: only key pattern present
p_key = self._attributes_t_wildcard2regex (tmp[0])
if len (tmp) == 2 :
# two elems: val pattern is also present
p_val = self._attributes_t_wildcard2regex (tmp[1])
# compile the found pattern
if len (p_key) : pc_key = re.compile (p_key)
if len (p_val) : pc_val = re.compile (p_val)
# now dig out matching keys. List hooks are triggered in
# _attributes_i_list(flow).
matches = []
for key in self._attributes_i_list (flow=flow) :
val = str(self._attributes_i_get (key, flow=flow))
if ( (pc_key == None) or pc_key.search (key) ) and \
( (pc_val == None) or pc_val.search (val) ) :
matches.append (key)
return matches
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_exists (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`attribute_exists` (key) for details.
Registered keys which have never been explicitly set to a value do not
exist for the purpose of this call.
"""
# make sure interface is ready to use
d = self._attributes_t_init ()
# check if we know about that attribute
if key in d['attributes'] :
if 'exists' in d['attributes'][key] :
if d['attributes'][key]['exists'] :
return True
return False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_is_extended (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
This method will check if the given key is extended, i.e. was registered
on the fly, vs. registered explicitly.
This method is not used by, and not exposed via the public API, yet.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
return d['attributes'][key]['extended']
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_is_private (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
This method will check if the given key is private, i.e. starts with an
underscore and 'allow_private' is enabled.
This method is not used by, and not exposed via the public API, yet.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
return d['attributes'][key]['private']
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_is_readonly (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see L{attribute_is_readonly} (key) for details.
This method will check if the given key is readonly, i.e. cannot be
'set'. The call will also return 'True' if the attribute is final
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
# check if we know about that attribute
if d['attributes'][key]['mode'] == FINAL or \
d['attributes'][key]['mode'] == READONLY :
return True
return False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_is_writeable (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`attribute_is_writable` (key) for details.
This method will check if the given key is writeable - i.e. not readonly.
"""
return not self._attributes_i_is_readonly (key, flow=flow)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_is_removable (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`attribute_is_removable` (key) for details.
'True' if the attrib is writeable and Extended.
"""
if self._attributes_i_is_writeable (key, flow=flow) and \
self._attributes_i_is_extended (key, flow=flow) :
return True
return False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_is_vector (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`attribute_is_vector` (key) for details.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
# check if we know about that attribute
if d['attributes'][key]['flavor'] == VECTOR :
return True
return False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (bool)
def _attributes_i_is_final (self, key, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
This method will query the 'final' flag for an attribute, which signals
that the attribute will never change again.
This method is not used by, and not exposed via the public API, yet.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
if FINAL == d['attributes'][key]['mode'] :
return True
# no final flag found -- assume non-finality!
return False
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
callable,
rus.one_of (_UP, _DOWN))
@rus.returns (int)
def _attributes_i_add_cb (self, key, cb, flow) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`add_callback` (key, cb) for details.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
d['attributes'][key]['callbacks'].append (cb)
id = len (d['attributes'][key]['callbacks']) - 1
if flow==self._DOWN :
self._attributes_t_call_caller (key, id, cb)
return id
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.optional (int),
rus.one_of (_UP, _DOWN))
@rus.returns (rus.nothing)
def _attributes_i_del_cb (self, key, id=None, flow=_DOWN) :
"""
This internal method should not be explicitly called by consumers of
this API, but is indirectly used via the different public interfaces.
see :func:`remove_callback` (key, cb) for details.
"""
# make sure interface is ready to use
d = self._attributes_t_init (key)
if flow==self._DOWN :
self._attributes_t_call_caller (key, id, None)
# id == None: remove all callbacks
if not id :
d['attributes'][key]['callbacks'] = []
else :
if len (d['attributes'][key]['callbacks']) < id :
raise se.BadParameter ("invalid callback cookie for attribute %s" % key)
else :
# do not pop from list, that would invalidate the id's!
d['attributes'][key]['callbacks'][id] = None
# --------------------------------------------------------------------------
#
# This part of the interface is primarily for use in deriving classes, which
# thus provide the Attributes interface.
#
# Keys should be provided as CamelCase (only relevant if camelcasing is
# set).
#
# Naming: _attributes_*
#
@rus.takes ('Attributes',
str,
rus.optional (rus.optional (rus.anything)),
rus.optional (rus.one_of (ANY, URL, INT, FLOAT, STRING, BOOL, ENUM, TIME)),
rus.optional (rus.one_of (ANY, SCALAR, VECTOR, DICT)),
rus.optional (rus.one_of (READONLY, WRITEABLE, ALIAS, FINAL)),
rus.optional (rus.one_of (bool, EXTENDED)),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_register (self, key, default=None, typ=ANY, flavor=ANY,
mode=WRITEABLE, ext=False, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Register a new attribute.
This function ignores extensible, private, final and readonly flags. It
can also be used to re-register an existing attribute with new
properties -- the old attribute value, callbacks etc. will be lost
though. Using this call that way may result in confusing behaviour on
the public API level.
"""
# FIXME: check for valid mode and flavor settings
# make sure interface is ready to use
d = self._attributes_t_init ()
priv = False
if d['private'] and key[0] == '_' :
priv = True
# we expect keys to be registered as CamelCase (in those cases where
# that matters). But we store attributes in 'under_score' version.
us_key = self._attributes_t_underscore (key)
# retain old values
val = default
exists = False
if default != None :
exists = True
if us_key in d['attributes'] :
val = d['attributes'][us_key]['value']
exists = True
# register the attribute and properties
d['attributes'][us_key] = {}
d['attributes'][us_key]['value'] = val # initial value
d['attributes'][us_key]['default'] = default # default value
d['attributes'][us_key]['type'] = typ # int, float, enum, ...
d['attributes'][us_key]['exists'] = exists # no value set, yet?
d['attributes'][us_key]['flavor'] = flavor # scalar / vector
d['attributes'][us_key]['mode'] = mode # readonly / writeable / final
d['attributes'][us_key]['extended'] = ext # is an extended attribute
d['attributes'][us_key]['private'] = priv # is a private attribute
d['attributes'][us_key]['camelcase'] = key # keep original key name
d['attributes'][us_key]['underscore'] = us_key # keep under_scored name
d['attributes'][us_key]['enums'] = [] # list of valid enum values
d['attributes'][us_key]['checks'] = [] # list of custom value checks
d['attributes'][us_key]['callbacks'] = [] # list of callbacks
d['attributes'][us_key]['recursion'] = False # recursion check for callbacks
d['attributes'][us_key]['setter'] = None # custom attribute setter
d['attributes'][us_key]['getter'] = None # custom attribute getter
d['attributes'][us_key]['last'] = never # time of last refresh (never)
d['attributes'][us_key]['ttl'] = 0.0 # refresh delay (none)
# for enum types, we add a value checker
if typ == ENUM :
def _enum_check (key, val) :
if None == val :
# None is always allowed
return True
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
vals = d['attributes'][us_key]['enums']
# check if there is anything to check
if not vals :
return True
# value must be one of allowed enums
if val in vals :
return True
# Houston, we got a problem...
msg = "incorrect value (%s) for Enum typed attribute (%s)." \
"Allowed values: %s" % (str(val), key, str(vals))
raise se.BadParameter (msg)
self._attributes_add_check (key, _enum_check, flow=flow)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
str,
rus.one_of (_UP, _DOWN))
@rus.returns (rus.nothing)
def _attributes_register_deprecated (self, key, alias, flow=_DOWN) :
"""
Often enough, there is the need to use change attribute names. It is
good practice to not simply rename attributes, and thus effectively
remove old ones, as that is likely to break existing code. Instead, new
names are added, and old names are kept for a certain time for backward
compatibility. To support migration to the new names, the old names
should be marked as 'deprecated' though - which can be configured to
print a warning whenever an old, deprecated attribute is used.
This method allows to register such deprecated attribute names. They
can thus be used just like new ones, and in fact are implemented as
aliases to the new ones -- but they will print a deprecated warning on
usage.
The first parameter is the old name of the attribute, the second
parameter is the aliased new name. Note that the new name needs to be
registered before (via :class:`saga._attributes_register`)::
# old code:
self._attributes_register ('apple', 'Appel', STRING, SCALAR, WRITEABLE)
# new code
self._attributes_register ('fruit', 'Appel', STRING, SCALAR, WRITEABLE)
self._attributes_register_deprecated ('apple', 'fruit')
In some cases, you may want to deprecate a variable and not replace it
with a new one. In order to keep this interface simple, this can be
achieved via::
# new code
self._attributes_register ('deprecated_apple', 'Appel', STRING, SCALAR, WRITEABLE)
self._attributes_register_deprecated ('apple', 'deprecated_apple')
This way, the user will either see a warning, or has to explicitly use
'deprecated_apple' as attribute name -- which should be warning enough,
at least for the programmer ;o)
"""
# we expect keys to be registered as CamelCase (in those cases where
# that matters). But we store attributes in 'under_score' version.
us_alias = self._attributes_t_underscore (alias)
us_key = self._attributes_t_underscore (key)
# make sure interface is ready to use
# This check will throw if 'alias' was not registered before.
d = self._attributes_t_init (us_alias)
# remove any old instance of this attribute
if us_key in d['attributes'] :
self._attributes_unregister (us_key, flow=flow)
# register the attribute and properties
d['attributes'][us_key] = {}
d['attributes'][us_key]['mode'] = ALIAS # alias
d['attributes'][us_key]['alias'] = us_alias # aliased var
d['attributes'][us_key]['camelcase'] = key # keep original key name
d['attributes'][us_key]['underscore'] = us_key # keep under_scored name
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (rus.nothing)
def _attributes_unregister (self, key, flow) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Unregister an attribute.
This function ignores the extensible, private, final and readonly flag,
and is supposed to be used by derived classes, not by the consumer of
the API.
Note that unregistering is different from setting the value to 'None' --
all meta information about the attribute will be removed. Further
attempts to access the attribute from the public API will result in an
DoesNotExist exception. This method should be used sparingly -- in
fact, GFD.90 requires final attributes to stay around forever (frozen).
"""
# make sure interface is ready to use
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
# if the attribute exists, purge it
if us_key in d['attributes'] :
del (d['attributes'][us_key])
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.one_of (_UP, _DOWN))
@rus.returns (rus.nothing)
def _attributes_remove (self, key, flow) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Remove an extended an attribute.
This function allows to safely remove any attribute which is 'private'
or 'extended' and has write permissions.
"""
# make sure interface is ready to use
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
if self._attributes_i_is_removable (key, flow=flow) :
del (d['attributes'][us_key])
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.optional (rus.list_of (rus.anything)),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_enums (self, key, enums=None, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Specifies the set of allowed values for Enum typed attributes. If not
set, or if list is None, any values are allowed.
"""
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
d['attributes'][us_key]['enums'] = enums
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
rus.optional (bool),
rus.optional (callable),
rus.optional (callable),
rus.optional (callable),
rus.optional (callable),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_extensible (self, e=True,
getter=None, setter=None,
lister=None, caller=None,
flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Allow (or forbid) the on-the-fly creation of new attributes. Note that
this method also allows to *remove* the extensible flag -- that leaves
any previously created extended attributes untouched, but just prevents
the creation of new extended attributes.
"""
d = self._attributes_t_init ()
d['extensible'] = e
if getter : self._attributes_set_global_getter (getter, flow=flow)
if setter : self._attributes_set_global_setter (setter, flow=flow)
if lister : self._attributes_set_global_lister (lister, flow=flow)
if caller : self._attributes_set_global_caller (caller, flow=flow)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
rus.optional (bool),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_allow_private (self, p=True, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Allow (or forbid) the on-the-fly creation of private attributes
(starting with underscore). Note that this method also allows to
*remove* the respective flag -- that leaves any previously created
private attributes untouched, but just prevents the creation of new
private attributes.
"""
d = self._attributes_t_init ()
d['private'] = p
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
rus.optional (bool),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_camelcasing (self, c=True, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Use 'CamelCase' for dict entries and the GFD.90 API, but 'under_score'
for properties.
Note that we do not provide an option to turn CamelCasing off - once it
is turned on, it stays on -- otherwise we would loose attributes...
"""
d = self._attributes_t_init ()
d['camelcasing'] = c
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
'Attributes',
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns ('Attributes')
def _attributes_deep_copy (self, other, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
This method can be used to make sure that deep copies of derived classes
are also deep copies of the respective attributes. In accordance with
GFD.90, the deep copy will ignore callbacks. It will copy checks
though, as the assumption is that value constraints stay valid.
Note that we don't copy private keys.
"""
# make sure interface is ready to use
d = self._attributes_t_init ()
other_d = {}
orig_d = other._attributes_t_init ()
# for some reason, deep copy won't work on the 'attributes' dict, so we
# do it manually. Use the list copy c'tor to copy list elements.
other_d['camelcasing'] = d['camelcasing']
other_d['attributes'] = d['attributes']
other_d['extensible'] = d['extensible']
other_d['private'] = d['private']
other_d['camelcasing'] = d['camelcasing']
other_d['recursion'] = d['recursion']
other_d['getter'] = d['setter']
other_d['setter'] = d['setter']
other_d['lister'] = d['lister']
other_d['caller'] = d['caller']
other_d['attributes'] = {}
for key in d['attributes'] :
other_d['attributes'][key] = {}
other_d['attributes'][key]['default'] = d['attributes'][key]['default']
other_d['attributes'][key]['exists'] = d['attributes'][key]['exists']
other_d['attributes'][key]['type'] = d['attributes'][key]['type']
other_d['attributes'][key]['flavor'] = d['attributes'][key]['flavor']
other_d['attributes'][key]['mode'] = d['attributes'][key]['mode']
other_d['attributes'][key]['extended'] = d['attributes'][key]['extended']
other_d['attributes'][key]['private'] = d['attributes'][key]['private']
other_d['attributes'][key]['camelcase'] = d['attributes'][key]['camelcase']
other_d['attributes'][key]['underscore'] = d['attributes'][key]['underscore']
other_d['attributes'][key]['enums'] = list (d['attributes'][key]['enums'])
other_d['attributes'][key]['checks'] = list (d['attributes'][key]['checks'])
other_d['attributes'][key]['callbacks'] = list (d['attributes'][key]['callbacks'])
other_d['attributes'][key]['recursion'] = d['attributes'][key]['recursion']
other_d['attributes'][key]['setter'] = d['attributes'][key]['setter']
other_d['attributes'][key]['getter'] = d['attributes'][key]['getter']
other_d['attributes'][key]['last'] = d['attributes'][key]['last']
other_d['attributes'][key]['ttl'] = d['attributes'][key]['ttl']
if d['attributes'][key]['private' ] and key in orig_d['attributes'] :
# don't copy private keys
other_d['attributes'][key] = orig_d['attributes'][key]
else :
other_d['attributes'][key]['value'] = copy.deepcopy (d['attributes'][key]['value'])
# set the new dictionary as state for copied class
_AttributesBase.__setattr__ (other, '_d', other_d)
return other
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
('Attributes', dict))
@rus.returns ('Attributes')
def __deepcopy__ (self, memo) :
other = Attributes ()
return self._attributes_deep_copy (other)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
rus.optional (str),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_dump (self, msg=None, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Debugging dump to stdout.
"""
# make sure interface is ready to use
d = self._attributes_t_init ()
keys_all = sorted (d['attributes'].keys ())
print("---------------------------------------")
print(str (type (self)))
if msg :
print("---------------------------------------")
print(msg)
print("---------------------------------------")
print(" %-30s : %s" % ("Extensible" , d['extensible']))
print(" %-30s : %s" % ("Private" , d['private']))
print(" %-30s : %s" % ("CamelCasing" , d['camelcasing']))
print("---------------------------------------")
keys_exist = []
for key in keys_all :
if 'exists' in d['attributes'][key] and \
d['attributes'][key]['exists'] :
keys_exist.append (key)
print("'Registered' attributes")
for key in keys_all :
if key not in keys_exist :
if not d['attributes'][key]['mode'] == ALIAS and \
not d['attributes'][key]['extended'] :
print(" %-30s [%6s, %6s, %9s, %3d]: %s" % \
(d['attributes'][key]['camelcase'],
d['attributes'][key]['type'],
d['attributes'][key]['flavor'],
d['attributes'][key]['mode'],
len(d['attributes'][key]['callbacks']),
d['attributes'][key]['value']
))
print("---------------------------------------")
print("'Existing' attributes")
keys_exist.sort ()
for key in keys_exist :
if not d['attributes'][key]['mode'] == ALIAS :
print(" %-30s [%6s, %6s, %9s, %3d]: %s" % \
(d['attributes'][key]['camelcase'],
d['attributes'][key]['type'],
d['attributes'][key]['flavor'],
d['attributes'][key]['mode'],
len(d['attributes'][key]['callbacks']),
d['attributes'][key]['value']
))
print("---------------------------------------")
print("'Extended' attributes")
for key in keys_all :
if key not in keys_exist :
if not d['attributes'][key]['mode'] == ALIAS and \
d['attributes'][key]['extended'] :
print(" %-30s [%6s, %6s, %9s, %3d]: %s" % \
(d['attributes'][key]['camelcase'],
d['attributes'][key]['type'],
d['attributes'][key]['flavor'],
d['attributes'][key]['mode'],
len(d['attributes'][key]['callbacks']),
d['attributes'][key]['value']
))
print("---------------------------------------")
print("'Deprecated' attributes (aliases)")
for key in keys_all :
if key not in keys_exist :
if d['attributes'][key]['mode'] == ALIAS :
print(" %-30s [%24s]: %s" % \
(d['attributes'][key]['camelcase'],
' ',
d['attributes'][key]['alias']
))
print("---------------------------------------")
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.optional (rus.anything),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_final (self, key, val=None, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
This method will set the 'final' flag for an attribute, signalling that
the attribute will never change again. The ReadOnly flag is ignored.
A final value can optionally be provided -- otherwise the attribute is
frozen with its current value.
Note that attributes_set_final() will trigger callbacks, even if the
value was not set, or did not change.
"""
# make sure interface is ready to use
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
newval = val
oldval = d['attributes'][us_key]['value']
if None == newval :
# freeze at current value unless indicated otherwise
val = oldval
# flag as final, and set the final value (this order to avoid races in
# callbacks)
d['attributes'][us_key]['mode'] = FINAL
self._attributes_i_set (us_key, val, flow=flow)
# callbacks are not invoked if the value did not change -- we take care
# of that here.
#
# if None == newval or oldval == newval :
self._attributes_t_call_cb (key, val)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.optional (float),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_ttl (self, key, ttl=0.0, flow=_DOWN) :
""" set attributes TTL in seconds (float) -- see L{_attributes_i_set} """
# make sure interface is ready to use.
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
d['attributes'][us_key]['ttl'] = ttl
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_add_check (self, key, check, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
Value checks can be added dynamically, and per attribute. 'callable'
needs to be a python callable, and will be invoked as::
callable (key, val)
Those checks will be invoked whenever a new attribute value is set. If
that call then returns 'True', the value is accepted. Otherwise, the
value will be considered to be invalid, which results in an exception as
per above. 'callable' can return a string as error message.
"""
# make sure interface is ready to use
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
# register the attribute and properties
d['attributes'][us_key]['checks'].append (check)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_getter (self, key, getter, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
The Attribute API makes no assumptions on how attribute values are kept
up-to-date. In general, it expects an asynchronous thread to set
attribute values as needed. To keep the complexity low for backend
developers, it also supports the registration of 'setter' and 'getter'
hooks. Those are very similar to callbacks, but kind of inversed: where
frontend callbacks are invoked on backend attribute changes, backend
hooks are invoked on frontend attribute queries. They are expected to
internally trigger state updates.
For example, on::
print(c.attrib)
The attribute getter for the 'attrib' attribute will be invoked. If for
that attribute a getter hook is registered, that hook will first query
the backend for value updates. After that update has been performed,
the getter will retrieve the (updated) value.
Similarly, setter hooks will be invoked *after* the attribute setter
method, to inform the implementation of the updated attribute value.
Further, list hooks are invoked before a list or find operation is
really internally executed, to allow the implementation to updated the
list of available attributes.
Note that only one setter/getter/lister method can be registered (for
setters/getters per key, for listers per class instance).
Hooks have a different call signature than callbacks::
setter (self, value)
getter (self)
global_setter (self, key, value)
global_setter (self, key)
global_lister (self)
global_caller (self, key)
Note that frequent setter, and even more list and getter calls are very
common -- the implementation of hooks should consider to cache the
respective values.
"""
# make sure interface is ready to use
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
# register the attribute and properties
d['attributes'][us_key]['getter'] = getter
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_setter (self, key, setter, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
See documentation of L{_attributes_set_getter } for details.
"""
# make sure interface is ready to use
us_key = self._attributes_t_underscore (key)
d = self._attributes_t_init (us_key)
# register the attribute and properties
d['attributes'][us_key]['setter'] = setter
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_global_lister (self, lister, flow) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
See documentation of L{_attributes_set_getter } for details.
"""
d = self._attributes_t_init ()
# register the attribute and properties
d['lister'] = lister
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_global_caller (self, caller, flow) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
See documentation of :class:`saga._attributes_set_setter ` for details.
"""
d = self._attributes_t_init ()
# register the attribute and properties
d['caller'] = caller
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_global_getter (self, getter, flow=_DOWN) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
See documentation of L{_attributes_set_getter } for details.
"""
d = self._attributes_t_init ()
# register the attribute and properties
d['getter'] = getter
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def _attributes_set_global_setter (self, setter, flow) :
"""
This interface method is not part of the public consumer API, but can
safely be called from within derived classes.
See documentation of L{_attributes_set_getter } for details.
"""
d = self._attributes_t_init ()
# register the attribute and properties
d['setter'] = setter
# --------------------------------------------------------------------------
#
# the GFD.90 attribute interface
#
# The GFD.90 interface supports CamelCasing, and thus converts all keys to
# underscore before using them.
[docs] @rus.takes ('Attributes',
str,
rus.anything,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def set_attribute (self, key, val, _flow=_DOWN) :
"""
set_attribute(key, val)
This method sets the value of the specified attribute. If that
attribute does not exist, DoesNotExist is raised -- unless the attribute
set is marked 'extensible' or 'private'. In that case, the attribute is
created and set on the fly (defaulting to mode=writeable, flavor=Scalar,
type=ANY, default=None). A value of 'None' may reset the attribute to
its default value, if such one exists (see documentation).
Note that this method is performing a number of checks and conversions,
to match the value type to the attribute properties (type, mode, flavor).
Those conversions are not guaranteed to yield the expected result -- for
example, the conversion from 'scalar' to 'vector' is, for complex types,
ambiguous at best, and somewhat stupid. The consumer of the API SHOULD
ensure correct attribute values. The conversions are intended to
support the most trivial and simple use cases (int to string etc).
Failed conversions will result in an BadParameter exception.
Attempts to set a 'final' attribute are silently ignored. Attempts to
set a 'readonly' attribute will result in an IncorrectState exception
being raised.
Note that set_attribute() will trigger callbacks, if a new value
(different from the old value) is given.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_set (us_key, val, flow=_flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.anything)
def get_attribute (self, key, _flow=_DOWN) :
"""
get_attribute(key)
This method returns the value of the specified attribute. If that
attribute does not exist, an DoesNotExist is raised. It is not an
error to query an existing, but unset attribute though -- that will
result in 'None' to be returned (or the default value, if available).
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_get (us_key, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.list_of (rus.anything),
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def set_vector_attribute (self, key, val, _flow=_DOWN) :
"""
set_vector_attribute (key, val)
See also: :func:`saga.Attributes.set_attribute` (key, val).
As python can handle scalar and vector types transparently, this method
is in fact not very useful. For that reason, it maps internally to the
set_attribute method.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_set (us_key, val, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.list_of (rus.anything))
def get_vector_attribute (self, key, _flow=_DOWN) :
"""
get_vector_attribute (key)
See also: :func:`saga.Attributes.get_attribute` (key).
As python can handle scalar and vector types transparently, this method
is in fact not very useful. For that reason, it maps internally to the
get_attribute method.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_get (us_key, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def remove_attribute (self, key, _flow=_DOWN) :
"""
remove_attribute (key)
Removing an attribute is actually different from unsetting it, or from
setting it to 'None'. On remove, all traces of the attribute are
purged, and the key will not be listed on
:func:`saga.Attributes.list_attributes` () anymore.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_remove (us_key, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.list_of (str))
def list_attributes (self, _flow=_DOWN) :
"""
list_attributes ()
List all attributes which have been explicitly set.
"""
return self._attributes_i_list (flow=_flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.list_of (str))
def find_attributes (self, pattern, _flow=_DOWN) :
"""
find_attributes (pattern)
Similar to list(), but also grep for a given attribute pattern. That
pattern is of the form 'key=val', where both 'key' and 'val' can contain
POSIX shell wildcards. For non-string typed attributes, the pattern is
applied to a string serialization of the typed value, if that exists.
"""
return self._attributes_i_find (pattern, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (bool)
def attribute_exists (self, key, _flow=_DOWN) :
"""
attribute_exist (key)
This method will check if the given key is known and was set explicitly.
The call will also return 'True' if the value for that key is 'None'.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_exists (us_key, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (bool)
def attribute_is_readonly (self, key, _flow=_DOWN) :
"""
attribute_is_readonly (key)
This method will check if the given key is readonly, i.e. cannot be
'set'. The call will also return 'True' if the attribute is final
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_is_readonly (us_key, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (bool)
def attribute_is_writeable (self, key, _flow=_DOWN) :
"""
attribute_is_writeable (key)
This method will check if the given key is writeable - i.e. not readonly.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_is_writeable (us_key, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (bool)
def attribute_is_removable (self, key, _flow=_DOWN) :
"""
attribute_is_writeable (key)
This method will check if the given key can be removed.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_is_removable (us_key, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (bool)
def attribute_is_vector (self, key, _flow=_DOWN) :
"""
attribute_is_vector (key)
This method will check if the given attribute has a vector value type.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_is_vector (us_key, _flow)
# --------------------------------------------------------------------------
#
# fold the GFD.90 monitoring API into the attributes API
#
[docs] @rus.takes ('Attributes',
str,
callable,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (int)
def add_callback (self, key, cb, _flow=_DOWN) :
"""
add_callback (key, cb)
For any attribute change, the API will check if any callbacks are
registered for that attribute. If so, those callbacks will be called
in order of registration. This registration function will return an
id (cookie) identifying the callback -- that id can be used to
remove the callback.
A callback is any callable python construct, and MUST accept three
arguments::
- STRING key: the name of the attribute which changed
- ANY val: the new value of the attribute
- ANY obj: the object on which this attribute interface was called
The 'obj' can be any python object type, but is guaranteed to expose
this attribute interface.
The callback SHOULD return 'True' or 'False' -- on 'True', the callback
will remain registered, and will thus be called again on the next
attribute change. On returning 'False', the callback will be
unregistered, and will thus not be called again. Returning nothing is
interpreted as 'False', other return values lead to undefined behavior.
Note that callbacks will not be called on 'Final' attributes (they will
be called once as that attribute enters finality).
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_add_cb (us_key, cb, _flow)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
str,
int,
rus.optional (rus.one_of (_UP, _DOWN)))
@rus.returns (rus.nothing)
def remove_callback (self, key, id, _flow=_DOWN) :
"""
remove_callback (key, id)
This method allows to unregister a previously registered callback, by
providing its id. It is not an error to remove a non-existing cb, but
a valid ID MUST be provided -- otherwise, a BadParameter is raised.
If no ID is provided (id == None), all callbacks are removed for this
attribute.
"""
key = self._attributes_t_keycheck (key)
us_key = self._attributes_t_underscore (key)
return self._attributes_i_del_cb (us_key, id, _flow)
# --------------------------------------------------------------------------
#
# Python property interface
#
# we assume that properties are always used in under_score notation.
#
@rus.takes ('Attributes',
str)
@rus.returns (rus.anything)
def __getattr__ (self, key) :
""" see L{get_attribute} (key) for details. """
key = self._attributes_t_keycheck (key)
return self._attributes_i_get (key, flow=self._DOWN)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str,
rus.anything)
@rus.returns (rus.nothing)
def __setattr__ (self, key, val) :
""" see L{set_attribute} (key, val) for details. """
key = self._attributes_t_keycheck (key)
return self._attributes_i_set (key, val, flow=self._DOWN)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes',
str)
@rus.returns (rus.nothing)
def __delattr__ (self, key) :
""" see L{remove_attribute} (key) for details. """
key = self._attributes_t_keycheck (key)
return self._attributes_remove (key, flow=self._DOWN)
# --------------------------------------------------------------------------
#
@rus.takes ('Attributes')
@rus.returns (str)
def __str__ (self) :
""" return a string representation of all set attributes """
s = "%s %s" % (type(self), str(self.as_dict()))
return s
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes',
dict)
@rus.returns (dict)
def from_dict (self, seed):
""" set attributes from dict """
for k,v in seed.items():
self.set_attribute(k,v)
# --------------------------------------------------------------------------
#
[docs] @rus.takes ('Attributes')
@rus.returns (dict)
def as_dict (self) :
""" return a dict representation of all set attributes """
d = {}
for a in self.list_attributes () :
d[a] = self.get_attribute (a)
return d
# --------------------------------------------------------------------------
#
# Python dictionary interface, via the DictMixin
#
# we assume that keys are always used in under_score notation.
#
# --------------------------------------------------------------------------
#
def __getitem__ (self, key) :
return self.get_attribute(key)
# --------------------------------------------------------------------------
#
def __setitem__ (self, key, value) :
return self.set_attribute (key, value)
# --------------------------------------------------------------------------
#
def __delitem__ (self, key) :
return self.remove_attribute (key)
# --------------------------------------------------------------------------
#
[docs] def keys (self) :
return self._attributes_i_list (CamelCase=False)
# --------------------------------------------------------------------------
#
def __iter__ (self) :
return self
# --------------------------------------------------------------------------
#
def __next__ (self) :
iterlist = self._attributes_i_list (CamelCase=False)
d = self._attributes_t_init ()
if d['_iterpos'] >= len(iterlist) :
d['_iterpos'] = 0
raise StopIteration
if not len(iterlist) :
d['_iterpos'] = 0
raise StopIteration
d['_iterpos'] += 1
return iterlist[d['_iterpos']-1]
# ------------------------------------------------------------------------------
# FIXME: add
# - class metric()
# - add_metric()
# - remove_metric()
# - fire_metric()
# - list_metrics()
# - get_metric()
# - list_callbacks()
# ------------------------------------------------------------------------------