#!/usr/bin/python
"""
These modifiers work as follows:

Private: A private method can only be called from a method defined in
the same class as the called method.

Protected: A protected method can only be called from a method defined
in the same or class derived from the called methods class.

Public: No access restrictions (essentially a no-op).

Programmers with a C++ or Java background will be familiar with these
concepts in those languages; these decorators attempt to emulate their
behavior.

Bugs:

1) These decorators will not tolerate other decorators because they
examine the call stack in order to determine the callers frame.  A
second decorator, either before or after the access decorator will
insert a stack frame, which the current version of these decorators
cannot handle.  Making sure decorators set their wrapper functions
__name__, __doc__ and append to its dictionary would be required at
least for interoperability.

2) As noted, staticmethod and classmethod cannot be handled by the
access decorators, not only because they are decorators themselves, but
because the current access decorators require access to an instance of
the class (self) in order to do method resolution.  classmethod support
could probably be added without too much difficulty but staticmethods,
because they have no self or cls, would be difficult.

3) Friend classes, as defined in C++.  These would probably be defined
as a class-level list of classes that may have private/protected access
to the given class's internals.  This should not be too hard to add.

4) Decorators for member variables -- these decorators can already be
applied to get_* and set_* methods for properties.  Overriding
__getattr__ may be a better solution for attribute access, however.

Copyright (c) 2007, TimeHorse Multimedia.  All rights reserved.
Licensed under the Lesser GPL.

"""

import inspect

def private(fn):
    """Mark an [instance] method as Private
    Private methods can only be called by other methods from the same class."""
    largs, vargs, varkw, defaults = inspect.getargspec(fn)
    if len(largs) < 1:
        # To Do: What about staticmethod and classmethod decorated methods??
        # To Do: What about allowing friends...???
        raise TypeError, "Private methods must have a valid self parameter."

    def do_fn(self, *args, **kargs):
        # From self's MRO
        mro = inspect.getmro(self.__class__)

        # Find the class that contains fn
        binding_class = None
        for cls in mro:
            if fn.__name__ in cls.__dict__:
                if do_fn is cls.__dict__[fn.__name__]:
                    binding_class = cls
                    break

        if not binding_class:
            raise TypeError, fn.__name__ + \
                  " is not bound to self or any of its bases."

        # Next, get 1 level of outer frame (last_frame)
        my_frame = inspect.currentframe()
        prior_frames = inspect.getouterframes(my_frame)
        if len(prior_frames) > 1:
            last_frame_tuple = prior_frames[1]
            last_frame, last_file, last_line, last_name, \
                        last_ctx_lines, last_ctx_idx = last_frame_tuple

        # Next, get outer frame's self (first param)
        pargs, largs, dargs, frame_locals = inspect.getargvalues(last_frame)
        if len(pargs) < 1 or pargs[0] not in frame_locals:
            raise TypeError, \
                  "Attempt to call a Private method out of context."
        else:
            last_self = frame_locals[pargs[0]]

        if last_self is not self:
            raise ValueError, \
                  "The self reference is not consistant with caller."

        # Since last_self is self, the same MRO holds
        # Find the last function (last_name) in MRO
        last_binding_class = None
        for cls in mro:
            if last_name in cls.__dict__:
                if getattr(self, last_name).im_func is cls.__dict__[last_name]:
                    last_binding_class = cls
                    break

        if not last_binding_class:
            raise TypeError, \
                  "Outer method is not bound to self or any of its bases."

        # last_binding must be the same class as the binding_class
        if last_binding_class is not binding_class:
            raise TypeError, \
                  "Attempt to call a Private method out of context."
        else:
            return fn(self, *args, **kargs)

    # Make decorated function look like original
    do_fn.__name__ = fn.__name__
    do_fn.__doc__ = fn.__doc__
    do_fn.__dict__.update(fn.__dict__)

    return do_fn

def protected(fn):
    """Mark an [instance] method as Protected
    Protected methods can only be called by other methods from the same
    or derived class."""
    largs, vargs, varkw, defaults = inspect.getargspec(fn)
    if len(largs) < 1:
        # To Do: What about staticmethod and classmethod decorated methods??
        # To Do: What about allowing friends...???
        raise TypeError, "Protected methods must have a valid self parameter."

    def do_fn(self, *args, **kargs):
        # From self's MRO
        mro = inspect.getmro(self.__class__)

        # Find the class that contains fn
        binding_class = None
        for cls in mro:
            if fn.__name__ in cls.__dict__:
                if do_fn is cls.__dict__[fn.__name__]:
                    binding_class = cls
                    break

        if not binding_class:
            raise TypeError, fn.__name__ + \
                  " is not bound to self or any of its bases."

        # Next, get 1 level of outer frame (last_frame)
        my_frame = inspect.currentframe()
        prior_frames = inspect.getouterframes(my_frame)
        if len(prior_frames) > 1:
            last_frame_tuple = prior_frames[1]
            last_frame, last_file, last_line, last_name, \
                        last_ctx_lines, last_ctx_idx = last_frame_tuple

        # Next, get outer frame's self (first param)
        pargs, largs, dargs, frame_locals = inspect.getargvalues(last_frame)
        if len(pargs) < 1 or pargs[0] not in frame_locals:
            raise TypeError, \
                  "Attempt to call a Protected method out of context."
        else:
            last_self = frame_locals[pargs[0]]

        if last_self is not self:
            raise ValueError, \
                  "The self reference is not consistant with caller."

        # Since last_self is self, the same MRO holds
        # Find the last function (last_name) in MRO
        last_binding_class = None
        for cls in mro:
            if last_name in cls.__dict__:
                if getattr(self, last_name).im_func is cls.__dict__[last_name]:
                    last_binding_class = cls
                    break

        if not last_binding_class:
            raise TypeError, \
                  "Outer method is not bound to self or any of its bases."

        # last_binding must be the same class as the binding_class
        if not issubclass(last_binding_class, binding_class):
            raise TypeError, \
                  "Attempt to call a Protected method out of context."
        else:
            return fn(self, *args, **kargs)

    # Make decorated function look like original
    do_fn.__name__ = fn.__name__
    do_fn.__doc__ = fn.__doc__
    do_fn.__dict__.update(fn.__dict__)

    return do_fn

def public(fn):
    """Mark an [instance] method as Public
    Public methods can be called by anyone and have no access restrictions."""
    def do_fn(self, *args, **kargs):
        return fn(self, *args, **kargs)

    # Make decorated function look like original
    do_fn.__name__ = fn.__name__
    do_fn.__doc__ = fn.__doc__
    do_fn.__dict__.update(fn.__dict__)

    return do_fn


