#!/usr/bin/env python
# -*- coding: utf-8 -*-
__all__ = ['MultiState']
class _MultiMeta(type):
def __new__(cls, *args):
if not isinstance(args[2]['_attributes'], tuple):
raise RuntimeError(
'"_attributes" must be a tuple!'
)
if 'set' in args[2]['_attributes']:
raise RuntimeError(
'Option "set" not in list of allowed attributes!'
)
for attr in args[2]['_attributes']:
if attr not in args[2]:
raise RuntimeError(
'Must set default value for option "{}" during subclass '
'creation!'.format(attr)
)
return super().__new__(cls, *args)
def __setattr__(cls, attr, value):
raise RuntimeError(
'Setting attributes directy is not allowed. Use "set" method!'
)
def __repr__(cls):
if hasattr(cls, '__repr__'):
return getattr(cls, '__repr__')()
else:
return super().__repr__()
def __str__(cls):
if hasattr(cls, '__str__'):
return getattr(cls, '__str__')()
else:
return super().__repr__()
[docs]class MultiState(object, metaclass=_MultiMeta):
'''
Multi state subclasses are used to manage global items.
`MultiState` is a so-called Singleton, which means that no instances
can be created. This way, it is guaranteed that only one `MultiState`
entity exists. Thus it offers the possibilty to handle a "global"
state, because from anywhere in the code, one could query the
current state and react to it. One usecase would be to
temporarily allow downloading of files, otherwise being forbidden.
The `MultiState` class is not useful on its own, but you have to
subclass it::
>>> from pycraf.utils import MultiState
>>> class MyState(MultiState):
...
... # define list of allowed attributes
... _attributes = ('foo', 'bar')
...
... # set default values
... foo = 1
... bar = "guido"
It is mandatory to provide a tuple of allowed attributes and to
create these instance attributes and assign default values.
During class creation, this will be validated, for convenience.
After defining the state class, one can do the following::
>>> MyState.foo
1
>>> MyState.set(foo=2)
<MultiState MyState>
>>> MyState.foo
2
The `set` method returns a context manager,::
>>> with MyState.set(foo="dave", bar=10):
... print(MyState.foo, MyState.bar)
dave 10
>>> print(MyState.foo, MyState.bar)
2 guido
which makes it possible to temporarily change the state object and
go back to the original value, once the `with` scope has ended.
Note, that one cannot set the attributes directly (to ensure
that the validation method is always run)::
>>> MyState.foo = 0
Traceback (most recent call last):
...
RuntimeError: Setting attributes directy is not allowed. Use "set" method!
Subclasses will generally override `validate` to convert from any
of the acceptable inputs (such as strings) to the appropriate
internal objects::
class MyState(MultiState):
# define list of allowed attributes
_attributes = ('foo', 'bar')
# set default values
foo = 1
bar = "guido"
@classmethod
def validate(cls, **kwargs):
assert isinstance(kwargs['foo'], int)
# etc.
return kwargs
Notes
-----
This class was adapted from the `~astropy.utils.state.ScienceState` class.
'''
_attributes = tuple()
def __init__(self):
raise RuntimeError('This class is a singleton. Do not instantiate.')
[docs] @classmethod
def set(cls, *, _do_validate=True, **kwargs):
"""
Set the current science state value.
"""
for k in kwargs:
if k not in cls._attributes:
raise ValueError(
'Option "{}" not in list of allowed attributes!'.format(k)
)
class _Context(object):
def __init__(self, parent, attrs):
self._parent = parent
self._values = {}
for k in attrs:
self._values[k] = getattr(parent, k)
def __enter__(self):
pass
def __exit__(self, type, value, tb):
cls.hook(**self._values)
for k, v in self._values.items():
self._parent.__class__.__class__.__setattr__(
self._parent, k, v
)
def __repr__(self):
return ('<MultiState {0}>'.format(self._parent.__name__))
ctx = _Context(cls, cls._attributes)
if _do_validate:
kwargs = cls.validate(**kwargs)
cls.hook(**kwargs)
for k, v in kwargs.items():
cls.__class__.__class__.__setattr__(cls, k, v)
return ctx
[docs] @classmethod
def validate(cls, **kwargs):
'''
Validate the keyword arguments and return the (converted) kwargs
dictionary.
You should override this method if you want to enable validation
of your attributes.
Notes
-----
One doesn't need to validate the following things, as it is already
take care of by the `MultiState` class:
- Check that each argument is assigned with a default.
- Check that the kwargs keys are in _attributes tuple.
'''
return kwargs
[docs] @classmethod
def hook(cls, **kwargs):
'''
A hook which is called everytime when attributes are about to change.
You should override this method if you want to enable pre-processing
or monitoring of your attributes. For example, one could use this
to react to attribute changes::
>>> from pycraf.utils import MultiState
>>> class MyState(MultiState):
...
... _attributes = ('foo', 'bar')
... foo = 1
... bar = "guido"
...
... @classmethod
... def hook(cls, **kwargs):
... if 'bar' in kwargs:
... if kwargs['bar'] != cls.bar:
... print('{} about to change: {} --> {}'.format(
... 'bar', kwargs['bar'], cls.bar
... ))
... # do stuff ...
>>> _ = MyState.set(bar="david")
bar about to change: david --> guido
>>> _ = MyState.set(bar="david")
>>> _ = MyState.set(bar="guido")
bar about to change: guido --> david
>>> with MyState.set(bar="david"):
... pass
bar about to change: david --> guido
bar about to change: guido --> david
'''
pass