# The MIT License (MIT)
#
# Copyright (c) 2006-2011 Matthew Zipay <mattz@ninthtest.net>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""The Aglyph assembler injects dependencies into application components.
Application components and their dependencies are defined in an
:class:`aglyph.context.Context`, which is used to initialize an
assembler.
An assembler provides thread-safe caching of **singleton** component
instances and **borg** component shared-states (i.e. instance
``__dict__`` references).
"""
from __future__ import with_statement
__author__ = "Matthew Zipay <mattz@ninthtest.net>"
__version__ = "1.0.0"
import functools
from inspect import isclass
import logging
from aglyph import AglyphError, resolve_dotted_name
from aglyph.compat import is_callable, new_instance
from aglyph.cache import ReentrantMutexCache
from aglyph.component import Evaluator, Reference, Strategy
__all__ = ["Assembler"]
_logger = logging.getLogger(__name__)
[docs]class Assembler(object):
"""Create application objects using type 2 (setter) and type 3
(constructor) dependency injection.
"""
_logger = logging.getLogger("%s.Assembler" % __name__)
def __init__(self, context):
"""*context* should be an :class:`aglyph.context.Context`."""
super(Assembler, self).__init__()
self._context = context
self._singleton_cache = ReentrantMutexCache()
self._borg_cache = ReentrantMutexCache()
self._assembly_stack = []
self._logger.info("initialized %s", self)
[docs] def assemble(self, component_id):
"""Return an instance of the component specified by
*component_id* with all of its dependencies provided.
*component_id* must be a valid "relative_module.identifier"
dotted-name string or a user-provided unique component
identifier.
"""
self._logger.info("assembling %r", component_id)
# check for circular dependency
if (component_id in self._assembly_stack):
raise AglyphError("circular dependency detected: %s" %
" => ".join(self._assembly_stack +
[component_id]))
self._assembly_stack.append(component_id)
self._logger.debug("current assembly stack: %s", self._assembly_stack)
try:
component = self._context[component_id]
instance = self._create(component)
if (component.strategy == Strategy.PROTOTYPE):
self._wire(instance, component)
return instance
finally:
self._assembly_stack.pop()
def _create(self, component):
"""Return an instance of *component*.
*component* is an :class:`aglyph.component.Component`.
"""
create = getattr(self, "_create_%s" % component.strategy, None)
if (create is None):
raise AglyphError("don't know how to create a %r component" %
component.strategy)
return create(component)
def _create_prototype(self, component):
"""Return an instance of the prototype *component*.
*component* is an :class:`aglyph.component.Component` having
``strategy="prototype"``.
A new instance is always created and initialized.
"""
self._logger.info("creating %s", component)
instance = self._initialize(component)
return instance
def _create_singleton(self, component):
"""Return an instance of the singleton *component*.
*component* is an :class:`aglyph.component.Component` having
``strategy="singleton"``.
If *component* has previously been assembled, the cached
instance is returned. Otherwise, a new instance is created,
initialized, wired, cached, and then returned.
"""
self._logger.info("creating %s", component)
with self._singleton_cache.lock:
instance = self._singleton_cache.get(component.component_id)
self._logger.debug("cached instance for %s is %s", component,
type(instance))
if (instance is None):
# singletons are initialized and wired once, then cached
instance = self._initialize(component)
self._wire(instance, component)
self._singleton_cache[component.component_id] = instance
return instance
def _create_borg(self, component):
"""Return an instance of the borg *component*.
*component* is an :class:`aglyph.component.Component` having
``strategy="borg"``.
A new instance is always created. If *component* has previously
been assembled, the cached shared-state is assigned to the new
instance's ``__dict__`` and the instance is returned. Otherwise,
the new instance is initialized and wired, it's ``__dict__`` is
cached, and then the instance is returned.
"""
self._logger.info("creating %s", component)
with self._borg_cache.lock:
shared_state = self._borg_cache.get(component.component_id)
self._logger.debug("cached shared-state for %s is %s", component,
type(shared_state))
if (shared_state is None):
# borgs are initialized and wired, then the state is cached
instance = self._initialize(component)
self._wire(instance, component)
self._borg_cache[component.component_id] = instance.__dict__
return instance
else:
# TODO: test fixtures for TypeError and AglyphError
# always create a new instance of borg
cls = resolve_dotted_name(component.dotted_name)
if (not isclass(cls)):
raise TypeError("expected class, not %s"
% type(cls).__name__)
elif (hasattr(cls, "__slots__")):
# old-style classes do not enforce __slots__, but if
# __slots__ is defined for an old-style class, assume the
# intent is clear and reject anyway
raise AglyphError("borg is not supoprted for classes that "
"define or inherit __slots__")
instance = new_instance(cls)
instance.__dict__ = shared_state
return instance
def _initialize(self, component):
"""Return a new *component* object initialized with its
dependencies.
*component* is an :class:`aglyph.component.Component`.
This method performs type 3 (constructor) injection.
"""
self._logger.info("initializing %s", component)
initializer = resolve_dotted_name(component.dotted_name)
(args, kwargs) = self._resolve_args_and_keywords(component)
if (isclass(initializer)):
instance = new_instance(initializer)
instance.__init__(*args, **kwargs)
return instance
else:
# use the __call__ protocol
return initializer(*args, **kwargs)
def _resolve_args_and_keywords(self, component):
"""Return the fully assembled/evaluated positional and keyword
arguments for *component*.
*component* is an :class:`aglyph.component.Component`.
The arguments returned from this method are ready to be passed
directly to an initializer.
"""
self._logger.debug("resolving args and keywords for %s", component)
args = tuple([self._resolve(arg) for arg in component.init_args])
keywords = dict([(n, self._resolve(v))
for (n, v) in component.init_keywords.items()])
return (args, keywords)
def _wire(self, instance, component):
"""Inject dependencies into *instance* using direct attribute
assignment, setter methods, and/or properties.
*instance* is an initialized object of the type defined by
*component* (which is an :class:`aglyph.component.Component`).
This method performs type 2 (setter) injection.
"""
self._logger.info("wiring %s", component)
for (attr_name, raw_attr_value) in component.attributes.items():
instance_attr = getattr(instance, attr_name)
attr_value = self._resolve(raw_attr_value)
if (is_callable(instance_attr)):
# this is a setter method
instance_attr(attr_value)
else:
# this is a simple attribute or property
setattr(instance, attr_name, attr_value)
def _resolve(self, value_spec):
"""Return the actual value of an initialization or attribute
value specification.
If *value_spec* is an :class:`aglyph.component.Reference`, the
:func:`assemble` method is called recursively to
assemble the specified component, which is then returned.
If *value_spec* is an :class:`aglyph.component.Evaluator`, it is
evaluated (which may also result in nested references being
assembled, as described above). The resulting value is returned.
If *value_spec* is a :func:`functools.partial`, it is called,
and the resulting value is returned.
In any other case, *value_spec* is returned **unchanged**.
"""
if (isinstance(value_spec, Reference)):
return self.assemble(value_spec)
elif (isinstance(value_spec, Evaluator)):
# need to pass a reference to the assembler since the
# evaluation may require further component assembly
return value_spec(self)
elif (isinstance(value_spec, functools.partial)):
return value_spec()
else:
return value_spec
[docs] def clear_singletons(self):
"""Evict all cached singleton component instances.
:returns: a list of evicted component IDs
"""
with self._singleton_cache.lock:
singleton_ids = list(self._singleton_cache.keys())
self._logger.info("evicting cached singletons %s", singleton_ids)
self._singleton_cache.clear()
return singleton_ids
[docs] def clear_borgs(self):
"""Evict all cached borg component shared-states.
:returns: a list of component IDs corresponding to the evicted
shared-state instance ``__dict__`` references
"""
with self._borg_cache.lock:
borg_ids = list(self._borg_cache.keys())
self._logger.info("evicting cached borg states %s", borg_ids)
self._borg_cache.clear()
return borg_ids
def __contains__(self, component_id):
"""Return ``True`` if the *component_id* is defined in this
assembler's context.
*component_id* must be a valid "relative_module.identifier"
dotted-name string or a user-provided unique component
identifier.
"""
return (component_id in self._context)
def __repr__(self):
return "%s:%s<%s>" % (self.__class__.__module__,
self.__class__.__name__, self._context)