Source code for aglyph.binder

# 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.

""".. versionadded:: 1.1.0

The Aglyph binder provides a much simpler and more compact alternative
to :class:`aglyph.assembler.Assembler`, :class:`aglyph.context.Context`,
and :class:`aglyph.component.Component` for programmatic configuration
of Aglyph.

"Binding" usually results in much less configuration code. An
:class:`aglyph.binder.Binder` also offers the same functionality as an
:class:`aglyph.assembler.Assembler`, so the same object used to
configure injection can be used to *perform* injection, further reducing
the amount of "bootstrap" code needed.

For example, the following two blocks exhibit identical behavior:

Using ``Assembler`` / ``Context`` / ``Component``::

    context = Context("context-id")
    assembler = Assembler(context)
    component = Component("my.package.MyClass")
    component.init_args.append("arg")
    component.init_keywords["kw"] = "value"
    component.attributes["set_spam"] = "eggs"
    context.add(component)
    my = assembler.assemble("my.package.MyClass")

Using ``Binder``::

    binder = Binder()
    binder.bind(MyClass).init("arg", kw="value").attributes(set_spam="eggs")
    my = binder.lookup(MyClass)

"""

__author__ = "Matthew Zipay <mattz@ninthtest.net>"
__version__ = "1.1.0"

import logging
from uuid import uuid4

from aglyph import format_dotted_name, identify_by_spec
from aglyph.assembler import Assembler
from aglyph.compat import ClassAndFunctionTypes
from aglyph.component import Component, Reference, Strategy
from aglyph.context import Context

__all__ = ["Binder"]

_logger = logging.getLogger(__name__)


[docs]class Binder(Assembler): """Configure application components *and* provide assembly services (see :func:`lookup`) for those components. """ _logger = logging.getLogger("%s.Binder" % __name__) def __init__(self, binder_id=None): """*binder_id* is a unique identifier for this instance. If it is not provided, a random ID is generated. """ if (binder_id is None): binder_id = uuid4() super(Binder, self).__init__(Context("binder:%s" % binder_id)) self._binder_id = binder_id @property
[docs] def binder_id(self): """a read-only property for the binder ID""" return self._binder_id
[docs] def bind(self, component_spec, to=None, strategy=Strategy.PROTOTYPE): """Define a component by associating the unique ID for *component_spec* with the dotted-name for *to*. *component_spec* any importable class or unbound function, or a user-defined identifier, that will serve as the :func:`lookup` key for objects of the component. *to* is the importable class or unbound function, or the dotted-name for the same, that will be called to product objects of the component. If *to* is not provided, the component ID and dotted-name will be identical. In this case *component_spec* **must** be a class, a function, or a dotted-name. *strategy* must be a recognized component assembly strategy, and defaults to :attr:`aglyph.component.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 :attr:`aglyph.component.Strategy.BORG` (*"borg"*) component assembly strategy is only supported for classes that **do not** define or inherit ``__slots__``! :returns: a :class:`_Binding` object that allows the component dependencies to be defined in chained-call fashion. """ component_id = identify_by_spec(component_spec) if (to is None): dotted_name = component_id else: dotted_name = identify_by_spec(to) component = Component(component_id, dotted_name, strategy) self._context.add(component) self._logger.debug("bound %r to %r (%r)", component_spec, to, dotted_name) return _Binding(component)
[docs] def lookup(self, component_spec): """Return an instance of the component specified by *component_spec* with all of its dependencies provided. *component_spec* must be an importable class or unbound function, or a user-defined unique identifier, that was previously bound by a call to :func:`bind`. """ self._logger.info("looking up %r", component_spec) component_id = identify_by_spec(component_spec) return self.assemble(component_id)
[docs]class _Binding(object): """.. note:: This class is not intended to be imported or created directly; instances are returned automatically from the :meth:`Binder.bind` method. A ``_Binding`` allows component dependencies to be defined in chained-call fashion:: Binder().bind(...).init(*args, **keywords).attributes(**keywords) Or, if you don't prefer chained calls:: binding = Binder().bind(...) binding.init(*args, **keywords) binding.attributes(**keywords) """ __slots__ = ["_component"] _logger = logging.getLogger("%s._Binding" % __name__) def __init__(self, component): """*component* is an :class:`aglyph.component.Component` that was created by a call to :meth:`Binder.bind`. """ self._component = component
[docs] def init(self, *args, **keywords): """Define the initialization dependencies (i.e. position and/or keyword arguments) for a component. *args* are the positional argument dependencies, and *keywords* are the keyword argument dependencies. .. warning:: Multiple calls to this method on the same instance are *not* cumulative; each call will **replace** the :attr:`aglyph.component.Component.init_args` list and the :attr:`aglyph.component.Component.init_keywords` map. """ resolve = self._resolve self._component.init_args = [resolve(arg) for arg in args] self._component.init_keywords = dict( [(keyword, resolve(arg)) for (keyword, arg) in keywords.items()]) if (self._logger.isEnabledFor(logging.DEBUG)): self._logger.debug("bound %d args, %s keywords for %s", len(args), list(keywords.keys()), self) return self
[docs] def attributes(self, **keywords): """Define the attribute dependencies (i.e. fields, setter methods, and/or properties) for a component. *keywords* are the field, setter method, and/or property dependencies. Each keyword name corresponds to the name of a simple attribute (field), setter method, or property. .. warning:: Multiple calls to this method on the same instance are *not* cumulative; each call will **replace** the :attr:`aglyph.component.Component.attributes` map. """ resolve = self._resolve self._component.attributes = dict( [(name, resolve(value)) for (name, value) in keywords.items()]) if (self._logger.isEnabledFor(logging.DEBUG)): self._logger.debug("bound %s attributes for %s", list(keywords.keys()), self) return self
def _resolve(self, obj): """Return an :class:`aglyph.component.Reference` if *obj* is a class or function; otherwise, return *obj* itself. """ if (isinstance(obj, ClassAndFunctionTypes)): return Reference(format_dotted_name(obj)) else: return obj def __repr__(self): return "%s:%s<%s>" % (self.__class__.__module__, self.__class__.__name__, self._component)