'''
Defines 'capabilities', pieces of functionality that an object needs which must
be injected. The receiver of the capability is called a :dfn:`capable`.
A given capability can be provided by more than one capability provider, but,
for a given set of providers, only one will be bound at a time. Logically, each
provider that provides the capability is asked, in a user-provided preference
order, whether it can provide the capability for the *specific* capable and the
first one which can provide the capability is bound to the object.
The core idea is dependency injection: a capability does not modify the capable:
the capable receives the provider and a reference to the capability provided,
but how the capable uses the provider is up to the capable. This is important
because the user of the capable should not condition its behavior on the
particular capability provider used, although it may change its behavior based
on which capabilities the capable has.
Note, that there may be some providers that lose their ability to provide a
capability after they have been bound to a capable. This loss should be
communicated with a `CannotProvideCapability` exception when the relevant
methods are called on the provider. This *may* allow certain operations to be
retried with a provider lower on the capability order, *but* a provider that
throws `CannotProvideCapability` may validly be asked if it can provide the
capability again -- if it *still* cannot provide the capability, it should
communicate that by returning `None` from its `provides_to` method.
Providers may keep state between calls to provide a capability but their
correctness must not depend on any ordering of method calls except that, of
course, their ``__init__`` is called first. For instance, a provider can retain
an index that it downloads to answer `provides_to`, but if that index can
expire, the provider should check for that and retrieve an updated index if
necessary.
'''
import six
from .utils import FCN
from itertools import chain, repeat
class _Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
_Singleton._instances[cls] = super(_Singleton, cls).__call__(*args, **kwargs)
return _Singleton._instances[cls]
[docs]class Capability(six.with_metaclass(_Singleton)):
'''
A capability.
'''
def __init__(self):
self._class_name = FCN(type(self))
def __str__(self):
return self._class_name
def __lt__(self, other):
if isinstance(other, Capability):
return self._class_name < other._class_name
return NotImplemented
class ProviderMeta(type):
def __init__(self, name, bases, dct):
super(ProviderMeta, self).__init__(name, bases, dct)
my_caps = set(self.provided_capabilities)
for base in bases:
for base_cap in base.provided_capabilities:
if base_cap not in my_caps:
my_caps.add(base_cap)
self.provided_capabilities = sorted(my_caps)
[docs]class Provider(metaclass=ProviderMeta):
'''
A capability provider.
In general, providers should do any general setup in their initializer, and setup for
any source passed into `provides_to` method if, in fact, the provider does provide the
needed capabilities
'''
provided_capabilities = []
[docs] def provides(self, cap, obj):
'''
Returns a provider of the given capability if it's one this provider provides;
otherwise, returns None.
Parameters
----------
cap : Capability
The capability to provide
obj : Capable
The object to provide the capability to
Returns
-------
Provider or None
'''
if cap in getattr(self, 'provided_capabilities', ()):
return self.provides_to(obj, cap)
return None
[docs] def provides_to(self, obj, cap):
'''
Returns a `Provider` if the provider provides a capability to the given object;
otherwise, returns `None`.
The default implementation always returns `None`. Implementers of `Provider`
should check they can actually provide the capability for the given object rather
than just that they *might* be able to.
It's best to do setup for providing the capability before exiting this method
rather than, for instance, in the methods of the returned provider when the
`Capable` is trying to use it.
Parameters
----------
obj : Capable
The object needing/wanting the capability
cap : Capability
The capability needed/wanted
Returns
-------
Provider or None
'''
return None
[docs]class Capable(object):
'''
An object which can have capabilities
'''
@property
def needed_capabilities(self):
'''
The list of needed capabilities. These should be treated as though they are
required for any of the object's methods.
'''
return []
@property
def wanted_capabilities(self):
'''
The list of wanted capabilities. These should be treated as though they are
optional. The `Capable` subclass must determine how to deal with the provider not
being available.
'''
return []
[docs] def accept_capability_provider(self, cap, provider):
'''
The `Capable` should replace any previously accepted provider with the one
given.
The capability *should* be checked to determine which capability is being
provided, even if only one is declared on the class, so that if a sub-class
defines a capability without defining how to accept it, then the wrong actions
won't be taken. In case the capability isn't recognized, it is generally better to
pass it to the super() implementation rather than failing to allow for
`cooperative multiple inheritance`__.
__ https://rhettinger.wordpress.com/2011/05/26/super-considered-super/
Parameters
----------
cap : Capability
the capabiilty
provider : Provider
the provider which provides `cap`
'''
raise NotImplementedError()
[docs]class CannotProvideCapability(Exception):
'''
Thrown by a *provider* when it cannot provide the capability during the
object's execution
'''
def __init__(self, cap, provider):
'''
Parameters
----------
cap : Capability
the capabiilty
provider : Provider
the provider which failed to provide `cap`
'''
super(CannotProvideCapability, self).__init__('Provider, {}, cannot, now, provide the capability, {}'
.format(provider, cap))
self._cap = cap
self._provider = provider
[docs]class NoProviderAvailable(Exception):
'''
Thrown when there is no provider available for a capabiilty
Attributes
----------
cap : Capability
The capability that was sought
receiver : Capable
The object for which the capability was sought
'''
def __init__(self, cap, receiver, providers):
'''
Parameters
----------
cap : Capability
The capability that was sought
receiver : Capable
The object for which the capability was sought
providers : list of Provider
Providers that were tried for the capability
'''
super(NoProviderAvailable, self).__init__(
f'No providers currently provide {cap} for {receiver} among {providers}')
self._cap = cap
self._receiver = receiver
@property
def capability(self):
return self._cap
@property
def receiver(self):
return self._receiver
[docs]class NoProviderGiven(Exception):
'''
Thrown by a `Capable` when a `Capability` is needed, but none has been
provided by a call to `~Capable.accept_capability_provider`
'''
def __init__(self, cap, receiver=None):
'''
Parameters
----------
cap : Capability
The capability that was sought
receiver : Capable
The object for which a capability was needed
'''
super(NoProviderGiven, self).__init__('No {} providers were given{}'
.format(cap, ' to ' + repr(receiver) if receiver else ''))
self._cap = cap
[docs]class UnwantedCapability(Exception):
'''
Thrown by a `Capable` when `~Capable.accept_capability_provider` is offered a provider
for a capability that it does not "want", meaning it doesn't have the code to use it.
This can happen when a sub-class of a Capable declares a needed capability without
overriding `~Capable.accept_capability_provider` to accept that capability.
'''
[docs]def provide(ob, provs):
'''
Provide capabilities to `ob` out of `provs`
Parameters
----------
ob : object
An object which may need capabilities
provs : list of Provider
The providers available
Raises
------
NoProviderAvailable
when there is no provider available
'''
if is_capable(ob):
for required, cap in chain(
zip(repeat(True), ob.needed_capabilities),
zip(repeat(False), ob.wanted_capabilities)):
provider = get_provider(ob, cap, provs)
if not provider and required:
raise NoProviderAvailable(cap, ob, provs)
if provider:
ob.accept_capability_provider(cap, provider)
[docs]def get_provider(ob, cap, provs):
'''
Get provider for a capabilty that can provide to the given object
Parameters
----------
ob : Capable
Object needing the capability
cap : Capability
Capability needed
provs : list of Provider
All providers available
Returns
-------
Provider
A provider of the given capability or `None`
'''
for provider in get_providers(cap, provs, ob):
return provider
return None
[docs]def get_providers(cap, provs, ob):
'''
Get providers for a capabilty
Parameters
----------
cap : Capability
Capability needed
provs : list of Provider
All providers available
Yields
------
Provider
A Provider that provides the given capability
'''
for p in provs:
provfn = p.provides(cap, ob)
if provfn:
yield provfn
[docs]def is_capable(ob):
'''
Returns true if the given object can accept capability providers
Parameters
----------
ob : object
An object which may be a `Capable`
Returns
-------
bool
True if the given object accepts capability providers of some kind. Otherwise,
false.
'''
return isinstance(ob, Capable)