# -*- coding: UTF-8 -*-
# Copyright (c) 2006, 2011, 2013-2018 Matthew Zipay.
#
# 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 creates application objects from component
definitions, injecting dependencies into those objects through
initialization arguments and keywords, attributes, setter methods,
and/or properties.
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, **borg** component shared-states (i.e. instance ``__dict__``
references), and **weakref** component instance weak references.
"""
__author__ = "Matthew Zipay <mattz@ninthtest.info>"
from collections import OrderedDict
from functools import partial
from inspect import isclass
import logging
import warnings
import weakref
try:
import threading
threading_ = threading
except ImportError:
import dummy_threading
threading_ = dummy_threading
warnings.warn(
"threading module is not available; aglyph.assembler.Assembler "
"operations will NOT be thread-safe!",
RuntimeWarning)
from autologging import logged, traced
from aglyph import (
AglyphError,
format_dotted_name,
_identify,
resolve_dotted_name,
__version__,
)
from aglyph._compat import is_string, name_of, new_instance
from aglyph.component import Evaluator, Reference
__all__ = ["Assembler"]
_log = logging.getLogger(__name__)
_log.debug("using %r", threading_)
# thread-local storage for assembly
_assembly = threading_.local()
[docs]@traced
@logged
class Assembler(object):
"""Create application objects using type 2 (setter) and type 3
(constructor) dependency injection.
"""
def __init__(self, context):
"""
:arg aglyph.context.Context context:
a context object mapping unique IDs to component and template
definitions
"""
#PYVER: arguments to super() are implicit in Python 3
super(Assembler, self).__init__()
self._context = context
self._caches = {
"singleton": _ReentrantMutexCache(),
"borg": _ReentrantMutexCache(),
"weakref": _ReentrantMutexCache(),
}
self.__log.info("initialized %s", self)
[docs] def assemble(self, component_spec):
"""Create an object identified by *component_spec* and inject
its dependencies.
:arg component_spec:
a unique component ID, or an object whose dotted name is a
unique component ID
:return:
a complete object with all of its resolved dependencies
:raise KeyError:
if *component_spec* does not identify a component in this
assembler's context
:raise aglyph.AglyphError:
if *component_spec* causes a circular dependency
If *component_spec* is a string, it is assumed to be a unique
component ID and is used as-is. Otherwise,
:func:`aglyph.format_dotted_name` is called to convert
*component_spec* into a dotted name string, which is assumed to
be the component's unique ID.
How a component object is assembled (created, initialized,
wired, and returned) is determined by the component's
:attr:`aglyph.component.Component.strategy`:
**"prototype"**
A new object is always created, initialized, wired, and
returned.
This is the default assembly strategy for Aglyph components.
**"singleton"**
If the component has been assembled already during the
current application lifetime **and** there has been no
intervening call to :meth:`clear_singletons`, then a cached
reference to the object is returned.
Otherwise, a new object is created, initialized, wired,
cached, and returned.
Singleton component objects are cached by their
:attr:`aglyph.component.Component.unique_id`.
.. note::
Assembly of singleton components is a thread-safe
operation.
**"borg"**
If the component has been assembled already during the
current application lifetime **and** there has been no
intervening call to :meth:`clear_borgs`, then a new instance
is created and a cached reference to the shared-state is
directly assigned to the instance ``__dict__``.
Otherwise, a new instance is created, initialized, and wired;
its instance ``__dict__`` is cached; and the instance is
returned.
Borg component instance shared-states are cached by their
:attr:`aglyph.component.Component.unique_id`.
.. note::
Assembly of borg components is a thread-safe operation.
.. warning::
The borg assembly strategy is **only** supported for
components whose objects have an instance ``__dict__``.
This means that components using builtin classes, or
components using classes that define or inherit a
``__slots__`` member, **cannot** be declated as borg
components.
.. versionadded:: 2.1.0
support for the "weakref" assembly strategy
**"weakref"**
In the simplest terms, this is a "prototype" that can exhibit
"singleton" behavior: as long as there is at least one "live"
reference to the assembled object in the application runtime,
then requests to assemble this component will return the same
(cached) object.
When the only reference to the assembled object that remains
is the cached (weak) reference, the Python garbage collector
is free to destroy the object, at which point it is
automatically removed from the Aglyph cache.
Subsequent requests to assemble the same component will cause
a new object to be created, initialized, wired, cached (as a
weak reference), and returned.
.. note::
Please refer to the :mod:`weakref` module for a detailed
explanation of weak reference behavior.
.. versionadded:: 2.0.0
**Either** :attr:`aglyph.component.Component.factory_name`
**or** :attr:`aglyph.component.Component.member_name` may be
defined to exercise more control over how a component object
is created and initialized. Refer to the linked documentation
for details.
.. note::
This method is called recursively to assemble any dependency
of *component_spec* that is defined as a
:class:`aglyph.component.Reference`.
"""
component_id = _identify(component_spec)
component = self._context.get_component(component_id)
if component is None:
raise KeyError(
"component %r is not defined in %s" %
(component_id, self._context))
# issues/3: check for circular dependency
if not hasattr(_assembly, "component_stack"):
_assembly.component_stack = []
if (component_id in _assembly.component_stack):
raise AglyphError(
"circular dependency detected: %s" %
" > ".join(_assembly.component_stack + [component_id]))
_assembly.component_stack.append(component_id)
self.__log.debug(
"current assembly stack: %r", _assembly.component_stack)
try:
obj = self._create(component)
self.__log.info("assembled %r", component_id)
return obj
finally:
_assembly.component_stack.pop()
def _create(self, component):
"""Create an object of *component*.
:arg aglyph.component.Component component:
a component definition
:return:
an initialized object
:raise AglyphError:
if *component* uses an unrecognized assembly strategy
This method delegates to the appropriate
``_create_\<strategy\>`` method for *component*.
"""
# allow AttributeError; there is sufficient checking in Component for
# unrecognized strategy, and getting here with one takes quite a bit of
# effort (see test_Assembler.test_cant_create_unrecognized_strategy)
create = getattr(self, "_create_%s" % component.strategy)
return create(component)
def _create_prototype(self, component):
"""Create and initialize a prototype object for *component*.
:arg aglyph.component.Component component:
a component definition having strategy="prototype"
:return:
an initialized prototype object
A new object is always created and initialized when a
"prototype" component is assembled.
This is the default assembly strategy for Aglyph components.
"""
obj = self._initialize(component)
self._wire(obj, component)
self._call_lifecycle_method("after_inject", obj, component.unique_id)
self.__log.info("created %r", component)
return obj
# issues/5: support "_imported" strategy when using member_name
# (objects of these components are created like prototypes though they
# exhibit singleton behavior as long as the containing module is
# referenced in sys.modules)
_create__imported = _create_prototype
def _create_singleton(self, component):
"""Return the singleton object for *component*.
:arg aglyph.component.Component component:
a component definition having strategy="singleton"
:return:
the singleton object with all its dependencies resolved
If *component* has previously been assembled, the cached object
is returned. Otherwise, a new object is created, initialized,
wired, cached, and then returned.
.. note::
Assembly of singleton components is a thread-safe operation.
"""
with self._caches["singleton"] as cache:
obj = cache.get(component.unique_id)
if obj is None:
# singletons are initialized and wired once, then cached
obj = self._initialize(component)
self._wire(obj, component)
self._call_lifecycle_method(
"after_inject", obj, component.unique_id)
cache[component.unique_id] = obj
self.__log.info(
"created and cached %r @ %x", component, id(obj))
else:
self.__log.info(
"retrieved %r @ %x from cache", component, id(obj))
return obj
def _create_borg(self, component):
"""Create and initialize a borg object for *component*.
:arg aglyph.component.Component component:
a component definition having strategy="borg"
:return:
the borg instance with all its dependencies resolved
A new instance is always created. If *component* has been
previously 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, its
``__dict__`` is cached, and then the instance is returned.
.. note::
Assembly of borg components is a thread-safe operation.
.. warning::
The borg assembly strategy is **only** supported for
components whose objects have an instance ``__dict__``.
This means that components using builtin classes, or
components using classes that define or inherit a
``__slots__`` member, **cannot** be declated as borg
components.
"""
with self._caches["borg"] as cache:
cached_obj = cache.get(component.unique_id)
if cached_obj is None:
# borgs are initialized and wired, then the state is cached
# (an object of the borg is actually cached, but this is just
# an implementation detail... it's just as effective a
# container as anything else, and it makes the implementation
# of clear_borgs() far less expensive - if we only cached the
# new_obj.__dict__, we'd need to actually assemble each borg in
# clear_borgs() in order to call any before_clear lifecycle
# methods)
new_obj = self._initialize(component)
self._wire(new_obj, component)
self._call_lifecycle_method(
"after_inject", new_obj, component.unique_id)
cache[component.unique_id] = new_obj
self.__log.info(
"created and cached shared-state for %r", component)
else:
self.__log.info(
"retrieved shared-state for %r from cache", component)
cls = self._resolve_initializer(component)
new_obj = (
new_instance(cls) if (component.member_name is None)
else cls)
new_obj.__dict__ = cached_obj.__dict__
return new_obj
def _create_weakref(self, component):
"""Return a weakref object for *component*.
:arg aglyph.component.Component component:
a component definition having strategy="weakref"
:return:
the weakref object with all its dependencies resolved
.. note::
The object returned by this method and, therefore, by
:meth:`assemble` is the **referent** (i.e. the object which
is referred to *by* the weak reference).
If *component* has previously been assembled **and** the
internally-cached weak reference is still live, then the cached
referent object is returned. Otherwise, a new object is created,
initialized, wired, cached as a weak reference, and then
returned.
.. warning::
While assembly of weakref components is a thread-safe
operation with respect to *explicit* modification of the
weakref cache (i.e. any other thread attempting to assemble
a weakref component or to :meth:`clear_weakrefs` will be
blocked until this method returns), the nature of weak
references means that entries may still "disappear" from
the cache *even while the cache lock is held.*
With respect to assembly, this means that a referent
component object may "disappear" (i.e. the weak reference
goes dead) even *after* the cache lock has been acquired
and the weak reference retrieved from the cache. Practically
speaking, this should be of no concern to callers, since
a valid object of the component will be returned either way.
Please refer to the :mod:`weakref` module for a detailed
explanation of weak reference behavior.
"""
with self._caches["weakref"] as cache:
ref = cache.get(component.unique_id)
if ref is not None:
obj = ref()
if obj is None:
# referent is dead; discard the cache entry
self.__log.debug(
"cached weak reference to object of %r is dead; "
"new object will be created",
component)
cache.pop(component.unique_id)
else:
obj = None
if obj is None:
# an object is initialized and wired whenever a weak reference
# to the abject does not exist or is dead; then a weak
# reference to the object is cached and the object (i.e. the
# referent) is returned
obj = self._initialize(component)
self._wire(obj, component)
self._call_lifecycle_method(
"after_inject", obj, component.unique_id)
cache[component.unique_id] = weakref.ref(obj)
self.__log.info(
"created and cached weak reference to %r @ %x",
component, id(obj))
else:
self.__log.info(
"retrieved %r @ %x from cached weak reference",
component, id(obj))
return obj
def _initialize(self, component):
"""Create a new *component* object initialized with its
dependencies.
:arg aglyph.component.Component component:
a component definition
:return:
an initialized object of *component*
This method performs **type 3 (constructor)** dependency
injection.
.. versionchanged:: 2.1.0
If *component* specifies a :attr:`Component.member_name`
**and** either :attr:`Component.args`
or :attr:`Component.keywords`, then a :class:`RuntimeWarning`
is issued.
"""
initializer = self._resolve_initializer(component)
if component.member_name is None:
(args, keywords) = self._resolve_args_and_keywords(component)
try:
# issues/2: always use the __call__ protocol to initialize
obj = initializer(*args, **keywords)
except Exception as e:
raise AglyphError(
"failed to initialize object of component %r" %
component.unique_id,
e)
else:
obj = initializer
if component.args or component.keywords:
msg = (
"ignoring args and keywords for component %r "
"(uses member_name assembly)")
self.__log.warning(msg, component.unique_id)
warnings.warn(msg % component.unique_id, RuntimeWarning)
return obj
def _resolve_initializer(self, component):
"""Return the object that is responsible for creating new
*component* objects.
:arg aglyph.component.Component component:
a component definition
:return:
a callable if *component.member_name* is undefined, else the
member itself (which may or may not be a callable)
.. note::
If *component.member_name* is defined, the returned object
may still be callable (for example, *component.member_name*
may name a class). However, Aglyph will not **call** the
member.
This allows injection of dependencies that are references to
callable objects like classes and functions.
"""
initializer = resolve_dotted_name(component.dotted_name)
access_name = component.factory_name or component.member_name
if access_name:
for name in access_name.split('.'):
initializer = getattr(initializer, name) # allow AttributeError
return initializer
def _resolve_args_and_keywords(self, component):
"""Assemble or evaluate all positional and keyword arguments
for *component*.
:arg aglyph.component.Component component:
a component definition
:return:
the fully-resolved (i.e. recursively assembled or evaluated)
positional and keyword arguments for the *component*
initializer
:rtype:
a 2-tuple ``(args, keywords)`` where ``args`` is an N-tuple
and ``keywords`` is a :obj:`dict`
The values returned from this method are ready to be passed
directly to the *component* initializer (see :meth:`_initialize`
and :meth:`_resolve_initializer`).
.. versionchanged:: 2.1.0
The returned 2-tuple ``(args, keywords)`` accounts for the
*component* parent (and parent-of-parent, etc.) arguments and
keywords.
For any given component, this method will always return the
"official" arguments and keywords that should be passed to
the initializer.
"""
resolve = self._resolve_value
args = tuple([resolve(arg) for arg in self._collect_args(component)])
collected_keywords = self._collect_keywords(component)
keywords = dict(
[(name, resolve(value))
for (name, value) in collected_keywords.items()])
return (args, keywords)
def _collect_args(self, component):
"""Return the positional arguments used to initialize objects of
*component*.
:arg :class:`aglyph.component.Component` component:
the component being initialized
:return:
the positional arguments for the *component* initializer,
taking into account any positional arguments described by
parent components/templates
:rtype:
:obj:`list`
"""
collected_args = component.args
parent = self._context.get(component.parent_id)
while parent is not None:
if parent.args:
# children extend parents (like partial functions)
collected_args = parent.args + collected_args
parent = self._context.get(parent.parent_id)
return collected_args
def _collect_keywords(self, component):
"""Return the keyword arguments used to initialize objects of
*component*.
:arg aglyph.component.Component component:
the component being initialized
:return:
the keyword arguments for the *component* initializer, taking
into account any keyword arguments described by parent
components/templates
:rtype:
:obj:`dict`
"""
collected_keywords = component.keywords
parent = self._context.get(component.parent_id)
while parent is not None:
if parent.keywords:
parent_keywords = dict(parent.keywords)
# children extend/override parents (like partial functions)
parent_keywords.update(collected_keywords)
collected_keywords = parent_keywords
parent = self._context.get(parent.parent_id)
return collected_keywords
def _wire(self, obj, component):
"""Inject dependencies into *obj* using direct attribute
assignment, setter methods, and/or properties.
:param obj:
an initialized object for *component*
:param aglyph.component.Component component:
a component definition
This method performs **type 2 (setter)** dependency injection.
.. versionchanged:: 2.1.0
This method accounts for any attributes defined in the
*component* parent (and parent-of-parent, etc.).
"""
resolve = self._resolve_value
collected_attributes = self._collect_attributes(component)
for (attr_name, raw_attr_value) in collected_attributes.items():
# prevent AttributeError - if attr_name names a slot that has not
# been initialized, we want obj_attr to fail the callable test so
# that setattr initializes the slot value
obj_attr = getattr(obj, attr_name, None)
attr_value = resolve(raw_attr_value)
if callable(obj_attr):
# this is a setter method
obj_attr(attr_value)
else:
# this is a simple attribute or property
setattr(obj, attr_name, attr_value)
def _collect_attributes(self, component):
"""Return the attributes used to wire objects of *component*.
:arg aglyph.component.Component component:
the component definition for the object being wired
:return:
the (ordered) attributes for wiring an object of *component*,
taking into account any attributes described by parent
components/templates
:rtype:
:class:`collections.OrderedDict`
"""
collected_items = list(component.attributes.items())
parent = self._context.get(component.parent_id)
while parent is not None:
if parent.attributes:
parent_items = list(parent.attributes.items())
# children extend/override parents (like partial functions)
collected_items = parent_items + collected_items
parent = self._context.get(parent.parent_id)
return OrderedDict(collected_items)
def _resolve_value(self, value_spec):
"""Assemble or evaluate the runtime value of an initialization
or attribute value specification.
:arg value_spec:
the value specified for a component initialization argument
or component attribute
If *value_spec* is an :class:`aglyph.component.Reference`, the
:meth:`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**.
"""
#PYVER: Python 2.7 type(value_spec) would just give <type 'instance'>
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, partial):
return value_spec()
else:
return value_spec
def _call_lifecycle_method(self, lifecycle_state, obj, component_id):
"""Determine which *obj* lifecycle method to call, and call it.
:arg str lifecycle_state:
a lifecycle state identifier recognized by Aglyph
:arg obj:
the object on which to call the lifecycle method
:arg str component_id:
the component unique ID for *obj*
"""
component = self._context[component_id]
lifecycle_method_names = self._get_lifecycle_method_names(
lifecycle_state, component)
if lifecycle_method_names:
self.__log.debug(
"considering %s method names %r for %r %s",
lifecycle_state, lifecycle_method_names, component_id, obj)
# now call the first lifecycle method that is defined for obj
for method_name in lifecycle_method_names:
obj_lifecycle_method = getattr(obj, method_name, None)
if obj_lifecycle_method is not None:
# issues/5: if the component specifies member_name, it is
# possible that an after_inject method could be called
# multiple times on the same object
if component.member_name:
msg = (
"component %r specifies member_name; it is "
"possible that the %s %s.%s() method may be "
"called MULTIPLE times on %r")
self.__log.warning(
msg, component_id, lifecycle_state,
component.member_name, method_name, obj)
warnings.warn(
msg % (
component_id, lifecycle_state,
component.member_name, method_name, obj),
RuntimeWarning)
try:
obj_lifecycle_method()
except Exception as e:
msg = "ignoring %s raised from %r"
self.__log.exception(
msg, e.__class__.__name__, obj_lifecycle_method)
warnings.warn(
msg % (e.__class__.__name__, obj_lifecycle_method),
RuntimeWarning)
else:
self.__log.info(
"called %s %r on %r %s",
lifecycle_state, obj_lifecycle_method,
component_id, obj)
finally:
# whether or not it raised an exception, the
# lifecycle state method has now been called
break
else:
# here, we've encountered a "preferred" lifecycle method
# name, but the object doesn't define it; while this may be
# expected/intended by the developer, it also may suggest
# that there is a better way to configure the context, so
# at least log a warning
self.__log.warning(
"%r %s does not define %s method %r",
component_id, obj, lifecycle_state, method_name)
else:
self.__log.info(
"no %s lifecycle methods specified for %s %r",
lifecycle_state, obj, component_id)
def _get_lifecycle_method_names(self, lifecycle_state, component):
"""Determine the preferred-order list of all lifecycle method
names that may be applicable for an object of *component*.
:arg str lifecycle_state:
a lifecycle state identifier recognized by Aglyph
:arg aglyph.component.Component component:
the component
"""
lifecycle_method_names = []
# 1. Component.<lifecycle_state>
method_name = getattr(component, lifecycle_state)
if method_name is not None:
lifecycle_method_names.append(method_name)
# (2) parent Template/Component.<lifecycle_state>
parent = self._context.get(component.parent_id)
while parent is not None:
method_name = getattr(parent, lifecycle_state)
if method_name is not None:
lifecycle_method_names.append(method_name)
# (3) parent-of-parent Template/Component.<lifecycle_state>
parent = self._context.get(parent.parent_id)
# (4) Context.<lifecycle_state>
method_name = getattr(self._context, lifecycle_state)
if method_name is not None:
lifecycle_method_names.append(method_name)
return lifecycle_method_names
[docs] def init_singletons(self):
"""Assemble and cache all singleton component objects.
.. versionadded: 2.1.0
:return:
the initialized singleton component IDs
:rtype:
:obj:`list`
This method may be called at any time to "prime" the internal
singleton cache. For example, to eagerly initialize all
singleton components for your application::
assembler = Assembler(my_context)
assembler.init_singletons()
.. note::
Only singleton components that do not *already* have cached
objects will be initialized by this method.
Initialization of singleton component objects is a
thread-safe operation.
"""
return self._init_cache("singleton")
[docs] def clear_singletons(self):
"""Evict all cached singleton component objects.
:return:
the evicted singleton component IDs
:rtype:
:obj:`list`
Aglyph makes the following guarantees:
#. All cached singleton objects' "before_clear" lifecycle
methods are called (if specified) when they are evicted from
cache.
#. The singleton cache will be empty when this method
terminates.
.. note::
Any exception raised by a "before_clear" lifecycle method is
caught, logged, and issued as a :class:`RuntimeWarning`.
Eviction of cached singleton component objects is a
thread-safe operation.
"""
return self._clear_cache("singleton")
[docs] def init_borgs(self):
"""Assemble and cache the shared-states for all borg component
objects.
.. versionadded: 2.1.0
:return:
the initialized borg component IDs
:rtype:
:obj:`list`
This method may be called at any time to "prime" the internal
borg cache. For example, to eagerly initialize all borg
component shared-states for your application::
assembler = Assembler(my_context)
assembler.init_borgs()
.. note::
Only borg components that do not *already* have cached
shared-states will be initialized by this method.
Initialization of borg component shared-states is a
thread-safe operation.
"""
return self._init_cache("borg")
[docs] def clear_borgs(self):
"""Evict all cached borg component shared-states.
:return:
the evicted borg component IDs
:rtype:
:obj:`list`
Aglyph makes the following guarantees:
#. All cached borg shared-states' "before_clear" lifecycle
methods are called (if specified) when they are evicted from
cache.
#. The borg cache will be empty when this method terminates.
.. note::
Any exception raised by a "before_clear" lifecycle method is
caught, logged, and issued as a :class:`RuntimeWarning`.
Eviction of cached borg component shared-states is a
thread-safe operation.
"""
return self._clear_cache("borg")
[docs] def clear_weakrefs(self):
"""Evict all cached weakref component objects.
:return:
the evicted weakref component IDs
:rtype:
:obj:`list`
Aglyph makes the following guarantees:
#. **IF** a cached weakref object is still available **AND**
the component definition specifies a "before_clear" lifecycle
method, Aglyph will call that method when the object is
evicted.
#. The weakref cache will be empty when this method terminates.
.. note::
Any exception raised by a "before_clear" lifecycle method is
caught, logged, and issued as a :class:`RuntimeWarning`.
Eviction of cached weakref component objects is a thread-safe
operation.
.. warning::
While eviction of weakref components is a thread-safe
operation with respect to *explicit* modification of the
weakref cache (i.e. any other thread attempting to
:meth:`assemble` a weakref component or to
``clear_weakrefs()`` will be blocked until this method
returns), the nature of weak references means that entries
may still "disappear" from the cache *even while the cache
lock is held.*
With respect to cache-clearing, this means that referent
component objects may no longer be available even *after* the
cache lock has been acquired and the weakref component IDs
(keys) are retrieved from the cache. Practically speaking,
this means that callers must be aware of two things:
#. Aglyph **cannot** guarantee that "before_clear" lifecycle
methods are called on weakref component objects, because
there is no guarantee that a cached weak references is
"live." (This is the nature of weak references.)
#. Aglyph will only return the component IDs of weakref
component objects that were "live" at the moment they were
cleared.
Please refer to the :mod:`weakref` module for a detailed
explanation of weak reference behavior.
"""
with self._caches["weakref"] as cache:
eligible_weakref_ids = list(cache.keys())
cleared_weakref_ids = []
try:
for weakref_id in eligible_weakref_ids:
ref = cache.pop(weakref_id)
obj = ref()
if obj is not None:
self._call_lifecycle_method(
"before_clear", obj, weakref_id)
cleared_weakref_ids.append(weakref_id)
obj = None
else:
self.__log.info(
"weak reference to object of component %r is "
"already dead; any before_clear method "
"for this component will NOT be called",
weakref_id)
ref = None
finally:
cache.clear()
return cleared_weakref_ids
def _init_cache(self, strategy):
"""Prime the cache for *strategy* objects.
:arg str strategy:
"singleton" or "borg"
.. note::
The "weakref" strategy is not explicitly supported here
because priming a weak reference cache is nonsensical.
"""
with self._caches[strategy] as cache:
component_ids = []
for component in self._context.iter_components(strategy):
if component.unique_id not in cache:
self.assemble(component.unique_id)
component_ids.append(component.unique_id)
return component_ids
def _clear_cache(self, strategy):
"""Evict all objects from the cache for *strategy* objects,
calling the "before_clear" lifecycle method for each object.
:arg str strategy:
"singleton", "borg", or "weakref"
"""
with self._caches[strategy] as cache:
component_ids = list(cache.keys())
try:
for component_id in component_ids:
obj = cache.pop(component_id)
self._call_lifecycle_method(
"before_clear", obj, component_id)
obj = None
finally:
cache.clear()
return component_ids
[docs] def __contains__(self, component_spec):
"""Tell whether or not the component identified by
*component_spec* is defined in this assembler's context.
:arg component_spec:
used to determine the dotted name or component unique ID
:return:
``True`` if *component_spec* identifies a component that is
defined in this assembler's context, else ``False``
.. note::
Any *component_spec* for which this method returns ``True``
can be assembled by this assembler.
Accordingly, this method will return ``False`` if
*component_spec* actually identifies a
:class:`aglyph.component.Template` defined in this
assembler's context.
"""
try:
component_id = _identify(component_spec)
except:
return False
else:
return self._context.get_component(component_id) is not None
def __str__(self):
return "<%s @%08x %s>" % (
name_of(self.__class__), id(self), self._context)
def __repr__(self):
return "%s.%s(%r)" % (
self.__class__.__module__, name_of(self.__class__), self._context)
@traced
@logged
class _ReentrantMutexCache(dict):
"""A mapping that uses a reentrant lock object for synchronization.
Atomic "check-then-act" operations can be performed by
acquiring the cache lock, performing the check-then-act sequence,
and finally releasing the cache lock.
If the lock is held, any attempt to acquire it by a thread OTHER
than the holding thread will block until the holding thread releases
the lock. (A reentrant mutex permits the same thread to acquire the
same lock more than once, allowing nested access to a shared
resource by a single thread.)
A ``_ReentrantMutexCache`` object acts as a `context manager
<https://docs.python.org/3/library/stdtypes.html#typecontextmanager>`_
using `the with statement
<https://docs.python.org/3/reference/compound_stmts.html#with>`_::
cache = _ReentrantMutexCache()
...
with cache:
# check-then-act
"""
def __init__(self):
#PYVER: arguments to super() are implicit under Python 3
super(_ReentrantMutexCache, self).__init__()
self.__lock = threading_.RLock()
def __enter__(self):
"""Acquire the cache lock."""
self.__lock.acquire()
return self
def __exit__(self, e_type, e_obj, tb):
"""Release the cache lock.
:arg e_type:
the exception class if an exception occurred while executing
the body of the ``with`` statement, else ``None``
:arg Exception e_value:
the exception object if an exception occurred while executing
the body of the ``with`` statement, else ``None``
:arg tb:
the traceback if an exception occurred while executing the
body of the ``with`` statement, else ``None``
.. note::
If an exception occurred, it will be logged but allowed to
propagate.
"""
self.__lock.release()
if e_obj is not None:
self.__log.error(
"exception occurred while cache lock was held: %s", e_obj)
def __str__(self):
return "<%s @%08x>" % (name_of(self.__class__), id(self))
def __repr__(self):
return "%s.%s()" % (
self.__class__.__module__, name_of(self.__class__))