Skip to content

Commit

Permalink
Optional dependency injection
Browse files Browse the repository at this point in the history
This adds support for optional dependencies using Keystone
dependency injection.

A new decorator for classes is provided.

 @dependency.optional('optional_api_1', 'optional_api_2')

If there's a provider for the dependency, the attribute
will be set to the provider instance, otherwise the attribute
will be set to None.

This can be used in combination with required dependencies.

Related-bug: #1223524
Change-Id: I2b987f10a7bc85efab136ae9e84606e666494246
  • Loading branch information
Brant Knudson committed Sep 16, 2013
1 parent 607b115 commit 5e04343
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 11 deletions.
73 changes: 62 additions & 11 deletions keystone/common/dependency.py
Expand Up @@ -17,6 +17,7 @@
REGISTRY = {}

_future_dependencies = {}
_future_optionals = {}


class UnresolvableDependencyException(Exception):
Expand Down Expand Up @@ -44,22 +45,30 @@ def __wrapped_init__(self, *args, **kwargs):
return wrapper


def _process_dependencies(obj):
# Any dependencies that can be resolved immediately are resolved.
# Dependencies that cannot be resolved immediately are stored for
# resolution in resolve_future_dependencies.

def process(obj, attr_name, unresolved_in_out):
for dependency in getattr(obj, attr_name, []):
if dependency not in REGISTRY:
# We don't know about this dependency, so save it for later.
unresolved_in_out.setdefault(dependency, []).append(obj)
continue

setattr(obj, dependency, REGISTRY[dependency])

process(obj, '_dependencies', _future_dependencies)
process(obj, '_optionals', _future_optionals)


def requires(*dependencies):
"""Inject specified dependencies from the registry into the instance."""
def wrapper(self, *args, **kwargs):
"""Inject each dependency from the registry."""
self.__wrapped_init__(*args, **kwargs)

for dependency in self._dependencies:
if dependency not in REGISTRY:
if dependency in _future_dependencies:
_future_dependencies[dependency] += [self]
else:
_future_dependencies[dependency] = [self]

continue

setattr(self, dependency, REGISTRY[dependency])
_process_dependencies(self)

def wrapped(cls):
"""Note the required dependencies on the object for later injection.
Expand All @@ -77,15 +86,56 @@ def wrapped(cls):
return wrapped


def optional(*dependencies):
"""Optionally inject specified dependencies from the registry into the
instance.
"""
def wrapper(self, *args, **kwargs):
"""Inject each dependency from the registry."""
self.__wrapped_init__(*args, **kwargs)
_process_dependencies(self)

def wrapped(cls):
"""Note the optional dependencies on the object for later injection.
The dependencies of the parent class are combined with that of the
child class to create a new set of dependencies.
"""

existing_optionals = getattr(cls, '_optionals', set())
cls._optionals = existing_optionals.union(dependencies)
if not hasattr(cls, '__wrapped_init__'):
cls.__wrapped_init__ = cls.__init__
cls.__init__ = wrapper
return cls

return wrapped


def resolve_future_dependencies(provider_name=None):
if provider_name:
# A provider was registered, so take care of any objects depending on
# it.
targets = _future_dependencies.pop(provider_name, [])
targets.extend(_future_optionals.pop(provider_name, []))

for target in targets:
setattr(target, provider_name, REGISTRY[provider_name])

return

# Resolve optional dependencies, sets the attribute to None if there's no
# provider registered.
for dependency, targets in _future_optionals.iteritems():
provider = REGISTRY.get(dependency)
for target in targets:
setattr(target, dependency, provider)

_future_optionals.clear()

# Resolve optional dependencies, raises UnresolvableDependencyException if
# there's no provider registered.
try:
for dependency, targets in _future_dependencies.iteritems():
if dependency not in REGISTRY:
Expand All @@ -106,3 +156,4 @@ def reset():

REGISTRY.clear()
_future_dependencies.clear()
_future_optionals.clear()
56 changes: 56 additions & 0 deletions keystone/tests/test_injection.py
Expand Up @@ -211,3 +211,59 @@ class P(object):
dependency.reset()

self.assertFalse(dependency.REGISTRY)

def test_optional_dependency_not_provided(self):
requirement_name = uuid.uuid4().hex

@dependency.optional(requirement_name)
class C1(object):
pass

c1_inst = C1()

dependency.resolve_future_dependencies()

self.assertIsNone(getattr(c1_inst, requirement_name))

def test_optional_dependency_provided(self):
requirement_name = uuid.uuid4().hex

@dependency.optional(requirement_name)
class C1(object):
pass

@dependency.provider(requirement_name)
class P1(object):
pass

c1_inst = C1()
p1_inst = P1()

dependency.resolve_future_dependencies()

self.assertIs(getattr(c1_inst, requirement_name), p1_inst)

def test_optional_and_required(self):
p1_name = uuid.uuid4().hex
p2_name = uuid.uuid4().hex
optional_name = uuid.uuid4().hex

@dependency.provider(p1_name)
@dependency.requires(p2_name)
@dependency.optional(optional_name)
class P1(object):
pass

@dependency.provider(p2_name)
@dependency.requires(p1_name)
class P2(object):
pass

p1 = P1()
p2 = P2()

dependency.resolve_future_dependencies()

self.assertIs(getattr(p1, p2_name), p2)
self.assertIs(getattr(p2, p1_name), p1)
self.assertIsNone(getattr(p1, optional_name))

0 comments on commit 5e04343

Please sign in to comment.