# 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 classes in this module are used to define components and their
dependencies.
"""
__author__ = "Matthew Zipay <mattz@ninthtest.net>"
__version__ = "1.0.0"
import functools
import logging
from aglyph.compat import is_callable, StringTypes, TextType
__all__ = ["Component", "Evaluator", "Reference", "Strategy"]
_logger = logging.getLogger(__name__)
[docs]class Strategy(object):
"""Define the component assembly strategies recognized by Aglyph.
The default component assembly strategy for Aglyph is
``Strategy.PROTOTYPE`` (*"prototype"*).
"""
PROTOTYPE = "prototype"
"""a new instance of the component is always created, initialized,
wired, and returned
"""
SINGLETON = "singleton"
"""only one instance is created, initialized, and wired; the
instance is cached the first time the component is assembled, and
subsequent assembly requests return the cached instance
"""
BORG = "borg"
"""a new instance is always created; the internal state is cached
the first time the component is assembled, and thereafter all new
instances share this same state
"""
_STRATEGIES = set([PROTOTYPE, SINGLETON, BORG])
[docs]class Reference(TextType):
"""A place-holder used to refer to another
:class:`aglyph.component.Component`.
A ``Reference`` is used as an alias to identify a component that is a
dependency of another component. The value of a ``Reference`` can be
either a dotted-name or a user-provided unique ID.
A ``Reference`` value MUST correspond to a component ID in the same
context.
A ``Reference`` can be used as an argument for an
:class:`aglyph.component.Evaluator`, and can be assembled directly
by an :class:`aglyph.assembler.Assembler`.
.. warning::
In Python versions < 3.0, a ``Reference`` representing a
dotted-name *must* consist only of characters in the ASCII
subset of the source encoding (see :pep:`0263`).
But in Python versions >= 3.0, a ``Reference`` representing a
dotted-name *may* contain non-ASCII characters
(see :pep:`3131`).
Because of this difference, the super class of ``Reference``
is "dynamic" with respect to the version of Python under which
Aglyph is running (:class:`unicode` under Python 2.5 - 2.7,
:class:`str` under Python >= 3.0). This documentation shows
the base class as ``unicode`` because the documentation
generator runs under Python 2.7.
"""
def __new__(cls, value):
return TextType.__new__(cls, value)
[docs]class Evaluator(object):
"""Enable lazy creation of objects.
An ``Evaluator`` is similar to a :func:`functools.partial` in that
they both collect a function and related arguments into a callable
object with a simplified signature that can be called repeatedly to
produce a new object.
*Unlike* a partial function, an ``Evaluator`` may have arguments
that are not truly "frozen," in the sense that any argument may be
defined as an :class:`aglyph.component.Reference`, a
:func:`functools.partial`, or even another ``Evaluator``, which
needs to be resolved (i.e. assembled/called) before calling *func*.
"""
__slots__ = ["_func", "_args", "_keywords"]
_logger = logging.getLogger("%s.Evaluator" % __name__)
def __init__(self, func, *args, **keywords):
"""An ``Evaluator`` is initialized similar to a
:func:`functools.partial`:
*func* must be a callable object that returns new objects,
*args* is a tuple of positional arguments, and *keywords*
is a mapping of keyword arguments.
"""
super(Evaluator, self).__init__()
if (not is_callable(func)):
raise TypeError("%s is not callable" % type(func).__name__)
self._func = func
self._args = args
self._keywords = keywords
self._logger.debug("initialized %s", self)
@property
[docs] def func(self):
"""a read-only property for the callable"""
return self._func
@property
[docs] def args(self):
"""a read-only property for the positional arguments"""
return self._args
@property
[docs] def keywords(self):
"""a read-only property for the keyword arguments"""
return self._keywords
def __call__(self, assembler):
"""Call ``func(*args, **keywords)`` and return the new object.
*assembler* must be a reference to an
:class:`aglyph.assembly.Assembler`, which is used to assemble
any :class:`aglyph.component.Reference` encountered in the
function arguments.
"""
self._logger.info("evaluating %s", self)
args = self._args
keywords = self._keywords
resolve = self._resolve
resolved_args = tuple([resolve(arg, assembler) for arg in args])
# keywords MUST be strings!
resolved_keywords = dict([(keyword, resolve(arg, assembler))
for (keyword, arg) in keywords.items()])
return self._func(*resolved_args, **resolved_keywords)
def _resolve(self, arg, assembler):
"""Return the resolved *arg*.
*arg* is a positional or keyword argument to the function.
*assembler* is an :class:`aglyph.assembly.Assembler` used to
assemble an object if *arg* is an
:class:`aglyph.component.Reference`.
*arg* is resolve in one of the following ways:
* If *arg* is a :class:`Reference`, it is assembled by
*assembler*.
* If *arg* is an ``Evaluator``, it is called to product its
value (*assembler* is passed as an argument).
* If *arg* is a :func:`functools.partial`, is is called to
produce its value.
* If *arg* is a dictionary or a sequence other than a string
type, each item is resolved and a collection of the
approrpriate type is returned.
* If none of the above cases apply, *arg* is returned unchanged.
An ``Evaluator`` can handle any level of nesting (e.g. a
:func:`functools.partial` within an ``Evaluator`` within another
``Evaluator``).
"""
self._logger.debug("resolving %r", arg)
if (isinstance(arg, Reference)):
return assembler.assemble(arg)
elif (isinstance(arg, Evaluator)):
return arg(assembler)
elif (isinstance(arg, functools.partial)):
return arg()
elif (isinstance(arg, dict)):
# either keys or values may themselves be References, partials, or
# Evaluators
resolve = self._resolve
return dict([(resolve(key, assembler), resolve(value, assembler))
for (key, value) in arg.items()])
elif (hasattr(arg, "__iter__") and (not isinstance(arg, StringTypes))):
resolve = self._resolve
# assumption: the iterable class supports initialization with
# __init__(iterable)
return arg.__class__([resolve(value, assembler) for value in arg])
else:
return arg
def __repr__(self):
return "%s:%s<%r %r %r>" % (self.__class__.__module__,
self.__class__.__name__, self._func,
self._args, self._keywords)
[docs]class Component(object):
"""Define a component and the dependencies needed to create a new
object of that component at runtime.
"""
__slots__ = ["attributes", "_component_id", "_dotted_name", "init_args",
"init_keywords", "_strategy"]
_logger = logging.getLogger("%s.Component" % __name__)
def __init__(self, component_id, dotted_name=None,
strategy=Strategy.PROTOTYPE):
"""Only a component ID is strictly required to define a
component.
*component_id* must be a valid "relative_module.identifier"
dotted-name string or a user-provided unique component
identifier.
*dotted_name*, if provided, must be a valid
"relative_module.identifier" dotted-name string. If it is not
provided, *component_id* is assumed to be a dotted-name.
*strategy* must be a recognized component assembly strategy, and
defaults to ``Strategy.PROTOTYPE`` (*"prototype"*) if not
specified.
Please see :class:`aglyph.component.Strategy` for a description
of the component assembly strategies supported by Aglyph.
.. warning::
The ``Strategy.BORG`` (*"borg"*) component assembly strategy
is only supported for classes that **do not** define or
inherit ``__slots__``!
Once a ``Component`` instance is initialized, the ``init_args``
(list), ``init_keywords`` (dict), and ``attributes`` (dict)
members can be modified in-place to define the dependencies that
must be injected into objects of this component at assembly
time. For example::
component = Component("http.client.HTTPConnection")
component.init_args.append("www.ninthtest.net")
component.init_args.append(80)
component.init_keywords["strict"] = True
component.attributes["set_debuglevel"] = 1
In Aglyph, a component may:
* be assembled directly by an
:class:`aglyph.assembler.Assembler`
* identify other components as dependencies (using
:class:`aglyph.component.Reference`)
* be used by other components as a dependency
* use any combination of the above behaviors
"""
super(Component, self).__init__()
self._component_id = component_id
if (dotted_name is not None):
self._dotted_name = dotted_name
else:
self._dotted_name = component_id
if (strategy in Strategy._STRATEGIES):
self._strategy = strategy
else:
raise ValueError("unrecognized assembly strategy %r" % strategy)
self.init_args = []
self.init_keywords = {}
self.attributes = {}
self._logger.debug("initialized %s", self)
@property
[docs] def component_id(self):
"""a read-only property for the component ID"""
return self._component_id
@property
[docs] def dotted_name(self):
"""a read-only property for the component dotted-name"""
return self._dotted_name
@property
[docs] def strategy(self):
"""a read-only property for the component assembly strategy"""
return self._strategy
def __repr__(self):
return "%s:%s<%r %r %r>" % (self.__class__.__module__,
self.__class__.__name__,
self._component_id, self._dotted_name,
self._strategy)