# Copyright 2009 Yannick Loiseau
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This is an internal DSL providing fluent interface to inject decoration, i.e.
decorate methods at runtime.

Method names can be specified directly or using a method
grabber, which introspect the class.

Examples
~~~~~~~~~

Given the class C, public methods can be decorated with the "log" decorator
by::

    decorate(PUBLIC_METHODS).of(C).using(log)

which is equivalent to::

    C.foo = log(C.foo)
    ...
    C.baz = log(C.baz)

or more usually::

    class C(object):
        ...

        @log
        def foo(self, ...):
            ...
        
        ...

        @log
        def baz(self, ...):
            ...


Methods begining with ``get_`` in classes A and B are decorated with "foo"
and "bar"::

    decorate(method_named("get_.*")).of(A, B).using(foo, bar)

equivalent to::

    A.get_spam = bar(foo(A.get_spam))
    ...
    A.get_egg = bar(foo(A.get_egg))
    B.get_spamegg = bar(foo(B.get_spamegg))
    ...

Finally one can set exceptions in grabbers, such as in::

    decorate(method_named("get_.*")).but_not('get_something').of(A).using(foo)

"""
__description__ = u"DSL for decorator injection"
__author__  = u"Yannick Loiseau"
__email__   = u"yloiseau@gmail.com"
__version__ = u"0.1"
__license__ = u"Apache License, Version 2.0"


import inspect
import re
import types

# Utils ======================================================================

def make_re_matcher(regexp):
    """
    Convert a regexp into a selector suitable for a Grabber object.
    """
    def re_matcher(name, obj=None):
        the_name = name
        if hasattr(obj, '__name__'):
            the_name = obj.__name__
        return regexp.match(the_name)
    re_matcher.__name__ = repr(regexp.pattern)
    return re_matcher

# Generic  Grabber ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

true = lambda *x: True
true.__name__ = "True"

false = lambda *x: False
false.__name__ = 'False'

def or_combined(selectors):
    """
    Return a new selector function as the OR reduction of the given selectors.
    """
    funct = lambda *x: any(s(*x) for s in selectors if s is not false)
    funct.__name__ = ' OR '.join( s.__name__ for s in selectors if s is not false)
    return funct

def and_combined(selectors):
    """
    Return a new selector function as the AND reduction of the given
    selectors.
    """
    funct = lambda *x: all(s(*x) for s in selectors if s is not true)
    funct.__name__ = ' AND '.join(s.__name__ for s in selectors if s is not true)
    return funct

def negate(selector):
    """
    Return a new selector matching the negation of the given selector.
    """
    if selector is false:
        return true
    if selector is true:
        return false
    funct = lambda *x: not selector(*x)
    funct.__name__ = 'NOT(%s)' % selector.__name__
    return funct

#-----------------------------------------------------------------------------

class GenericGrabber(object):
    """
    Object to collect objects matching some criteria 
    
    - ``matchers`` are string, regexps or boolean functions that select the
                object, taking the name and the object (list is OR);
    - ``exceptions`` are matchers that exclude some objects from the list.

    To evaluate the grabber on a single object (i.e. test if it matches or not),
    use the :meth:`match` method or call the grabber (instances are callable)
    """
    _inspect_predicate = None

    def __init__(self, matchers, exceptions=list()):

        self._matcher = false

        for matcher in matchers:
            self.add_matcher(matcher)
        for exception in exceptions:
            self.add_exception(exception)

    def __and__(self, other):
        """
        Combine two grabbers into a new grabber that return common results.
        """
        return self.__class__([and_combined((self.match, other.match))])

    def __or__(self, other):
        """
        Combine two grabbers into a new grabber that return both results.
        """
        return  self.__class__([or_combined((self.match, other.match))])

    @staticmethod
    def _create_matcher(matcher):
        """
        Create a suitable matcher from a string or a regexp.
        If ``matcher`` is callable, return it as-is.
        """
        if isinstance(matcher, types.StringTypes):
            matcher = re.compile(matcher)
        if hasattr(matcher, 'match'):
            matcher = make_re_matcher(matcher)
        if callable(matcher):
            return matcher
        raise TypeError('grabbers matches must be regexp, string or callable')

    def add_matcher(self, matcher):
        """
        Add a matcher to the grabber. ``matcher`` can be a regexp
        (compiled or string) or any callable taking the object name
        and the object itself as returned by inspect.getmembers
        and returning a boolean.
        """
        self._matcher = or_combined((self._matcher,
                                     self._create_matcher(matcher)))

    def add_exception(self, matcher):
        """
        Add an exception to the grabber. ``matcher`` can be a regexp (compiled or
        string) or any callable taking the object name and the object itself
        and returning a boolean.
        """
        self._matcher = and_combined((self._matcher,
                                     negate(self._create_matcher(matcher))))

    @property
    def match(self):
        """
        Apply the matcher
        """
        return self._matcher

    def __call__(self, *args):
        return self.match(*args)
        
    def grab(self, container):
        """
        Generator on matching object names. MUST be overloaded.
        """
        raise NotImplementedError



# Selectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def isboundedto(obj):
    """
    Return a matcher returning True if the given method is bounded to an instance of ``obj``
    (can be used to filter class methods).
    """
    def bounded(name, meth):
        return (
            inspect.ismethod(meth) and
            hasattr(meth, 'im_self') and 
            isinstance(meth.im_self, obj)
        )
    bounded.__name__ = "isboundedto(%r)" % obj.__name__
    return bounded

def isfunction(name, obj):
    """
    Return True if ``obj`` is a function.
    """
    return inspect.isfunction(obj)

isunbounded = isboundedto(type(None))
isunbounded.__name__ = 'isunbounded'
isunbounded.__doc__ = "Return True if the method is unbounded."

def _isdecorationcandidate(*args):
    return (
        inspect.ismethod(*args) or
        inspect.isfunction(*args)
    )

def issubclassof(supercls):
    """
    Return a matcher returning True if the argument is a subclass of
    ``supercls``.
    """
    funct = lambda name, cls: issubclass(cls, supercls)
    funct.__name__ = "issubclassof(%r)" % supercls.__name__
    return funct

def isstrictsubclassof(supercls):
    """
    Return a matcher returning True if the argument is a subclass of
    ``supercls`` but not the superclass itself.
    """
    funct = lambda name, cls: issubclass(cls, supercls) and not cls is supercls
    funct.__name__ = "isstrictsubclassof(%r)" % supercls.__name__
    return funct


# Model ======================================================================

class ClassGrabber(GenericGrabber):
    """
    Grabber to collect classes to be decorated from a module
    """
    def grab_module(self, module, recurse=False):
        """ 
        Grabb classes from the given module. 
        Search submodules if ``recurse`` is True.
        """
        for name, cls in inspect.getmembers(module, inspect.isclass):
            if self.match(name, cls):
                yield cls

        if recurse:
            for modname, submodule in inspect.getmembers(module, 
                                                         inspect.ismodule):
                for cls in self.grab_module(submodule, recurse):
                    yield cls

    def grab_scope(self, elements, recurse=False):  
        """
        Grabb classes from the given scope (a dict, such as returned by
        :func:`globals()`. 
        Also search in modules if ``recurse`` is True.
        """
        for name, element in elements.items():
            if inspect.isclass(element) and self.match(name, element):
                yield element
            
            if recurse and inspect.ismodule(element):
                for cls in self.grab_module(element, recurse):
                    yield cls

    def grab(self, source, recurse=False):
        """
        Grabb from the given source, a scope or a module.
        Return an iterator on class objects.
        """
        if inspect.ismodule(source):    
            return self.grab_module(source, recurse)
        if hasattr(source, 'items'):
            return self.grab_scope(source, recurse)
        raise TypeError(
            "ClassGrabber.grab argument must be a module or a scope (dict)")

#-----------------------------------------------------------------------------
class MethodsGrabber(GenericGrabber):
    """
    Grabber to collect methods to be docorated from a class.
    """
    def grab(self, cls):
        """
        Grabb methods from the given class.
        Return an iterator on method names.
        """
        for name, obj in inspect.getmembers(cls, _isdecorationcandidate):
            if self.match(name, obj):
                yield name

#-----------------------------------------------------------------------------
class MethodDecorator(object):
    """
    Object to decorate the class. Usually built by the DSL.
    It uses the given grabber on every given classe to decorate the obtained
    method with every given decorator.
    """
    def __init__(self, grabber,
                       classes=list(), 
                       decorators=list(),
                       force_decoration=False):

        self._grabber = None
        self._classes = []
        self._decorators = []

        self.force_decoration = force_decoration

        self.set_grabber(grabber)
        for cls in classes:
            self.add_class(cls)
        for deco in decorators:
            self.add_decorator(deco)

    def add_class(self, cls):
        """
        Add a class to decorate.
        """
        if not isinstance(cls, (types.TypeType, types.ClassType)):
            raise TypeError('the class must be a type or a class instance')
        self._classes.append(cls)

    def add_decorator(self, deco):
        """
        Add a decorator to decorate methods with.
        """
        self._decorators.append(deco)

    def set_grabber(self, grabber):
        """
        Set the method grabber.
        """
        if not isinstance(grabber, MethodsGrabber):
            raise TypeError(
                    'methods_grabber must be a MethodsGrabber instance'
                    )
        self._grabber = MethodsGrabber([grabber.match])

    def add_exception(self, ex):
        """
        Add a exception to the grabber (delegate to the grabber).
        """
        self._grabber.add_exception(ex)

    def _decorate(self, cls, meth_name, decorator):
        """
        Effectively decorate:
        replace the method of the class by the decorated version.
        """
        meth = getattr(cls, meth_name)
        if not hasattr(meth, 'decorated'):
            meth.__dict__['decorated'] = []

        if decorator in meth.decorated and not self.force_decoration:
            return 
    
        setattr(cls, 
                meth_name,
                decorator(getattr(cls, meth_name)))
        meth.decorated.append(decorator)

    def decorate(self):
        """
        Decorate every matching methods of the classes with every decorator.
        """
        for cls in self._classes:
            for deco in self._decorators:
                for meth_name in self._grabber.grab(cls):
                    self._decorate(cls, meth_name, deco)


# DSL ========================================================================

# Predefined Grabbers

ALL_METHODS = MethodsGrabber(['.*'])
ALL_METHODS.__doc__ = """
Select all methods of the class
"""

PUBLIC_METHODS = MethodsGrabber(['^[^_].*'])
PUBLIC_METHODS.__doc__ = """
Select public methods of the class, i.e. not begining with ``_``
"""

PRIVATE_METHODS = MethodsGrabber(['^__.*[^_]$'])
PRIVATE_METHODS.__doc__ = """
Select private methods of the class, i.e. begining with ``__`` and not ending
with ``__''
"""

PROTECTED_METHODS = MethodsGrabber(['^_[^_].*[^_]$'])
PROTECTED_METHODS.__doc__ = """
Select protected methods of the class, i.e. begining with just 1 ``_``
"""

SPECIAL_METHODS = MethodsGrabber(['^__.+__$'])
SPECIAL_METHODS.__doc__ = """
Select special methods of the class, i.e. begining and ending with ``__``
"""

STATIC_METHODS = MethodsGrabber([isfunction])
STATIC_METHODS.__doc__ = """
Select static methods of the class
"""

CLASS_METHODS = MethodsGrabber([isboundedto(types.TypeType)])
CLASS_METHODS.__doc__ = """
Select class methods of the class
"""

NORMAL_METHODS = MethodsGrabber([isunbounded])
NORMAL_METHODS.__doc__ = """
Select "normal" methods of the class
"""

#-----------------------------------------------------------------------------

def method_named(*matches):
    """
    Contruct a MethodGrabber based on names or regexp.
    """
    return MethodsGrabber(matches)

class decorate(object):
    """
    Start a fluent interface "sentence". Take a method specification as argument
    (i.e. a :class:`MethodsGrabber`, one of the predefined or as returned by
    :func:`method_named`, or a user defined one).
    """
    def __init__(self, grabber):
        self._method_decorator = MethodDecorator(grabber)

    def of(self, *classes):
        """
        Define the list of classes to search method in. Can be classes
        (the object, not the name) or a :class:`ClassGrabber` object.
        """
        for cls in classes:
            if isinstance(cls, ClassGrabberDSL):
                for cls_ in cls:
                    self._method_decorator.add_class(cls_)
            else:
                self._method_decorator.add_class(cls)
        return self

    def using(self, *decorators):
        """
        Set the decorators to be used. The last element of the sentence.
        """
        for deco in decorators:
            self._method_decorator.add_decorator(deco)
        self._method_decorator.decorate()

    def but_not(self, *exceptions):
        """
        Add exceptions to the method specified in :class:`decorate`, in the form
        of regexp strings.
        To be used before :meth:`of`.
        """
        for ex in exceptions:
            self._method_decorator.add_exception(ex)
        return self

    @property
    def allowing_multiple_decoration(self):
        """
        Force multiple decoration by the same decorator.
        """
        self._method_decorator.force_decoration = True
        return self


class ClassGrabberDSL(object):
    def __init__(self, matchers=[]): 
        self._recurse = False
        self.grabber = ClassGrabber(matchers)
        self.grabber.add_exception(
                lambda name, cls: cls.__module__ == self.__module__)
        self.source = None

    @property
    def recursively(self):
        """
        Search recursively in modules and submodules.
        """
        self._recurse = True
        return self

    def within(self, source):
        """
        Define the source to grabb classes from. Can be a module or a scope
        (dict) such as returned by :func:`globals()`.
        """
        self.source = source
        return self

    def __iter__(self):
        return self.grabber.grab(self.source, self._recurse)

ALL_CLASSES = ClassGrabberDSL(['.*'])

def classes_named(*matches):
    """
    Select classes based on their name.
    """
    return ClassGrabberDSL(matches)

def subclasses_of(cls):
    """
    Select classes based on their superclass.
    """
    return ClassGrabberDSL([isstrictsubclassof(cls)])
