# -*- test-case-name: twisted.test.test_explorer -*-
# $Id: explorer.py,v 1.6 2003/02/18 21:15:30 acapnotic Exp $
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.


"""Support for python object introspection and exploration.

Note that Explorers, what with their list of attributes, are much like
manhole.coil.Configurables.  Someone should investigate this further. (TODO)

Also TODO: Determine how much code in here (particularly the function
signature stuff) can be replaced with functions available in the
L{inspect} module available in Python 2.1.
"""

# System Imports
import inspect, string, sys, types
import UserDict

# Twisted Imports
from twisted.spread import pb
from twisted.python import reflect


True=(1==1)
False=not True

class Pool(UserDict.UserDict):
    def getExplorer(self, object, identifier):
        oid = id(object)
        if self.data.has_key(oid):
            # XXX: This potentially returns something with
            # 'identifier' set to a different value.
            return self.data[oid]
        else:
            klass = typeTable.get(type(object), ExplorerGeneric)
            e = types.InstanceType(klass, {})
            self.data[oid] = e
            klass.__init__(e, object, identifier)
            return e

explorerPool = Pool()

class Explorer(pb.Cacheable):
    properties = ["id", "identifier"]
    attributeGroups = []
    accessors = ["get_refcount"]

    id = None
    identifier = None

    def __init__(self, object, identifier):
        self.object = object
        self.identifier = identifier
        self.id = id(object)

        self.properties = []
        reflect.accumulateClassList(self.__class__, 'properties',
                                    self.properties)

        self.attributeGroups = []
        reflect.accumulateClassList(self.__class__, 'attributeGroups',
                                    self.attributeGroups)

        self.accessors = []
        reflect.accumulateClassList(self.__class__, 'accessors',
                                    self.accessors)

    def getStateToCopyFor(self, perspective):
        all = ["properties", "attributeGroups", "accessors"]
        all.extend(self.properties)
        all.extend(self.attributeGroups)

        state = {}
        for key in all:
            state[key] = getattr(self, key)

        state['view'] = pb.ViewPoint(perspective, self)
        state['explorerClass'] = self.__class__.__name__
        return state

    def view_get_refcount(self, perspective):
        return sys.getrefcount(self)

class ExplorerGeneric(Explorer):
    properties = ["str", "repr", "typename"]

    def __init__(self, object, identifier):
        Explorer.__init__(self, object, identifier)
        self.str = str(object)
        self.repr = repr(object)
        self.typename = type(object).__name__


class ExplorerImmutable(Explorer):
    properties = ["value"]

    def __init__(self, object, identifier):
        Explorer.__init__(self, object, identifier)
        self.value = object


class ExplorerSequence(Explorer):
    properties = ["len"]
    attributeGroups = ["elements"]
    accessors = ["get_elements"]

    def __init__(self, seq, identifier):
        Explorer.__init__(self, seq, identifier)
        self.seq = seq
        self.len = len(seq)

        # Use accessor method to fill me in.
        self.elements = []

    def get_elements(self):
        self.len = len(self.seq)
        l = []
        for i in xrange(self.len):
            identifier = "%s[%s]" % (self.identifier, i)

            # GLOBAL: using global explorerPool
            l.append(explorerPool.getExplorer(self.seq[i], identifier))

        return l

    def view_get_elements(self, perspective):
        # XXX: set the .elements member of all my remoteCaches
        return self.get_elements()


class ExplorerMapping(Explorer):
    properties = ["len"]
    attributeGroups = ["keys"]
    accessors = ["get_keys", "get_item"]

    def __init__(self, dct, identifier):
        Explorer.__init__(self, dct, identifier)

        self.dct = dct
        self.len = len(dct)

        # Use accessor method to fill me in.
        self.keys = []

    def get_keys(self):
        keys = self.dct.keys()
        self.len = len(keys)
        l = []
        for i in xrange(self.len):
            identifier = "%s.keys()[%s]" % (self.identifier, i)

            # GLOBAL: using global explorerPool
            l.append(explorerPool.getExplorer(keys[i], identifier))

        return l

    def view_get_keys(self, perspective):
        # XXX: set the .keys member of all my remoteCaches
        return self.get_keys()

    def view_get_item(self, perspective, key):
        if type(key) is types.InstanceType:
            key = key.object

        item = self.dct[key]

        identifier = "%s[%s]" % (self.identifier, repr(key))
        # GLOBAL: using global explorerPool
        item = explorerPool.getExplorer(item, identifier)
        return item


class ExplorerBuiltin(Explorer):
    """
    @ivar name: the name the function was defined as
    @ivar doc: function's docstring, or C{None} if unavailable
    @ivar self: if not C{None}, the function is a method of this object.
    """
    properties = ["doc", "name", "self"]
    def __init__(self, function, identifier):
        Explorer.__init__(self, function, identifier)
        self.doc = function.__doc__
        self.name = function.__name__
        self.self = function.__self__


class ExplorerInstance(Explorer):
    """
    Attribute groups:
        - B{methods} -- dictionary of methods
        - B{data} -- dictionary of data members

    Note these are only the *instance* methods and members --
    if you want the class methods, you'll have to look up the class.

    TODO: Detail levels (me, me & class, me & class ancestory)

    @ivar klass: the class this is an instance of.
    """
    properties = ["klass"]
    attributeGroups = ["methods", "data"]

    def __init__(self, instance, identifier):
        Explorer.__init__(self, instance, identifier)
        members = {}
        methods = {}
        for i in dir(instance):
            # TODO: Make screening of private attributes configurable.
            if i[0] == '_':
                continue
            mIdentifier = string.join([identifier, i], ".")
            member = getattr(instance, i)
            mType = type(member)

            if mType is types.MethodType:
                methods[i] = explorerPool.getExplorer(member, mIdentifier)
            else:
                members[i] = explorerPool.getExplorer(member, mIdentifier)

        self.klass = explorerPool.getExplorer(instance.__class__,
                                              self.identifier +
                                              '.__class__')
        self.data = members
        self.methods = methods


class ExplorerClass(Explorer):
    """
    @ivar name: the name the class was defined with
    @ivar doc: the class's docstring
    @ivar bases: a list of this class's base classes.
    @ivar module: the module the class is defined in

    Attribute groups:
        - B{methods} -- class methods
        - B{data} -- other members of the class
    """
    properties = ["name", "doc", "bases", "module"]
    attributeGroups = ["methods", "data"]
    def __init__(self, theClass, identifier):
        Explorer.__init__(self, theClass, identifier)
        if not identifier:
            identifier = theClass.__name__
        members = {}
        methods = {}
        for i in dir(theClass):
            if (i[0] == '_') and (i != '__init__'):
                continue

            mIdentifier = string.join([identifier, i], ".")
            member = getattr(theClass, i)
            mType = type(member)

            if mType is types.MethodType:
                methods[i] = explorerPool.getExplorer(member, mIdentifier)
            else:
                members[i] = explorerPool.getExplorer(member, mIdentifier)

        self.name = theClass.__name__
        self.doc = inspect.getdoc(theClass)
        self.data = members
        self.methods = methods
        self.bases = explorerPool.getExplorer(theClass.__bases__,
                                              identifier + ".__bases__")
        self.module = getattr(theClass, '__module__', None)


class ExplorerFunction(Explorer):
    properties = ["name", "doc", "file", "line","signature"]
    """
        name -- the name the function was defined as
        signature -- the function's calling signature (Signature instance)
        doc -- the function's docstring
        file -- the file the function is defined in
        line -- the line in the file the function begins on
    """
    def __init__(self, function, identifier):
        Explorer.__init__(self, function, identifier)
        code = function.func_code
        argcount = code.co_argcount
        takesList = (code.co_flags & 0x04) and 1
        takesKeywords = (code.co_flags & 0x08) and 1

        n = (argcount + takesList + takesKeywords)
        signature = Signature(code.co_varnames[:n])

        if function.func_defaults:
            i_d = 0
            for i in xrange(argcount - len(function.func_defaults),
                            argcount):
                default = function.func_defaults[i_d]
                default = explorerPool.getExplorer(
                    default, '%s.func_defaults[%d]' % (identifier, i_d))
                signature.set_default(i, default)

                i_d = i_d + 1

        if takesKeywords:
            signature.set_keyword(n - 1)

        if takesList:
            signature.set_varlist(n - 1 - takesKeywords)

        # maybe also: function.func_globals,
        # or at least func_globals.__name__?
        # maybe the bytecode, for disassembly-view?

        self.name = function.__name__
        self.signature = signature
        self.doc = inspect.getdoc(function)
        self.file = code.co_filename
        self.line = code.co_firstlineno


class ExplorerMethod(ExplorerFunction):
    properties = ["self", "klass"]
    """
    In addition to ExplorerFunction properties:
        self -- the object I am bound to, or None if unbound
        klass -- the class I am a method of
    """
    def __init__(self, method, identifier):

        function = method.im_func
        if type(function) is types.InstanceType:
            function = function.__call__.im_func

        ExplorerFunction.__init__(self, function, identifier)
        self.id = id(method)
        self.klass = explorerPool.getExplorer(method.im_class,
                                              identifier + '.im_class')
        self.self = explorerPool.getExplorer(method.im_self,
                                             identifier + '.im_self')

        if method.im_self:
            # I'm a bound method -- eat the 'self' arg.
            self.signature.discardSelf()


class ExplorerModule(Explorer):
    """
    @ivar name: the name the module was defined as
    @ivar doc: documentation string for the module
    @ivar file: the file the module is defined in

    Attribute groups:
        - B{classes} -- the public classes provided by the module
        - B{functions} -- the public functions provided by the module
        - B{data} -- the public data members provided by the module

    (\"Public\" is taken to be \"anything that doesn't start with _\")
    """
    properties = ["name","doc","file"]
    attributeGroups = ["classes", "functions", "data"]

    def __init__(self, module, identifier):
        Explorer.__init__(self, module, identifier)
        functions = {}
        classes = {}
        data = {}
        for key, value in module.__dict__.items():
            if key[0] == '_':
                continue

            mIdentifier = "%s.%s" % (identifier, key)

            if type(value) is types.ClassType:
                classes[key] = explorerPool.getExplorer(value,
                                                        mIdentifier)
            elif type(value) is types.FunctionType:
                functions[key] = explorerPool.getExplorer(value,
                                                          mIdentifier)
            elif type(value) is types.ModuleType:
                pass # pass on imported modules
            else:
                data[key] = explorerPool.getExplorer(value, mIdentifier)

        self.name = module.__name__
        self.doc = inspect.getdoc(module)
        self.file = getattr(module, '__file__', None)
        self.classes = classes
        self.functions = functions
        self.data = data

typeTable = {types.InstanceType: ExplorerInstance,
             types.ClassType: ExplorerClass,
             types.MethodType: ExplorerMethod,
             types.FunctionType: ExplorerFunction,
             types.ModuleType: ExplorerModule,
             types.BuiltinFunctionType: ExplorerBuiltin,
             types.ListType: ExplorerSequence,
             types.TupleType: ExplorerSequence,
             types.DictType: ExplorerMapping,
             types.StringType: ExplorerImmutable,
             types.NoneType: ExplorerImmutable,
             types.IntType: ExplorerImmutable,
             types.FloatType: ExplorerImmutable,
             types.LongType: ExplorerImmutable,
             types.ComplexType: ExplorerImmutable,
             }

class Signature(pb.Copyable):
    """I represent the signature of a callable.

    Signatures are immutable, so don't expect my contents to change once
    they've been set.
    """
    _FLAVOURLESS = None
    _HAS_DEFAULT = 2
    _VAR_LIST = 4
    _KEYWORD_DICT = 8

    def __init__(self, argNames):
        self.name = argNames
        self.default = [None] * len(argNames)
        self.flavour = [None] * len(argNames)

    def get_name(self, arg):
        return self.name[arg]

    def get_default(self, arg):
        if arg is types.StringType:
            arg = self.name.index(arg)

        # Wouldn't it be nice if we just returned "None" when there
        # wasn't a default?  Well, yes, but often times "None" *is*
        # the default, so return a tuple instead.
        if self.flavour[arg] == self._HAS_DEFAULT:
            return (True, self.default[arg])
        else:
            return (False, None)

    def set_default(self, arg, value):
        if arg is types.StringType:
            arg = self.name.index(arg)

        self.flavour[arg] = self._HAS_DEFAULT
        self.default[arg] = value

    def set_varlist(self, arg):
        if arg is types.StringType:
            arg = self.name.index(arg)

        self.flavour[arg] = self._VAR_LIST

    def set_keyword(self, arg):
        if arg is types.StringType:
            arg = self.name.index(arg)

        self.flavour[arg] = self._KEYWORD_DICT

    def is_varlist(self, arg):
        if arg is types.StringType:
            arg = self.name.index(arg)

        return (self.flavour[arg] == self._VAR_LIST)

    def is_keyword(self, arg):
        if arg is types.StringType:
            arg = self.name.index(arg)

        return (self.flavour[arg] == self._KEYWORD_DICT)

    def discardSelf(self):
        """Invoke me to discard the first argument if this is a bound method.
        """
        ## if self.name[0] != 'self':
        ##    log.msg("Warning: Told to discard self, but name is %s" %
        ##            self.name[0])
        self.name = self.name[1:]
        self.default.pop(0)
        self.flavour.pop(0)

    def getStateToCopy(self):
        return {'name': tuple(self.name),
                'flavour': tuple(self.flavour),
                'default': tuple(self.default)}

    def __len__(self):
        return len(self.name)

    def __str__(self):
        arglist = []
        for arg in xrange(len(self)):
            name = self.get_name(arg)
            hasDefault, default = self.get_default(arg)
            if hasDefault:
                a = "%s=%s" % (name, default)
            elif self.is_varlist(arg):
                a = "*%s" % (name,)
            elif self.is_keyword(arg):
                a = "**%s" % (name,)
            else:
                a = name
            arglist.append(a)

        return string.join(arglist,", ")





class CRUFT_WatchyThingie:
    # TODO:
    #
    #  * an exclude mechanism for the watcher's browser, to avoid
    #    sending back large and uninteresting data structures.
    #
    #  * an exclude mechanism for the watcher's trigger, to avoid
    #    triggering on some frequently-called-method-that-doesn't-
    #    actually-change-anything.
    #
    #  * XXX! need removeWatch()

    def watchIdentifier(self, identifier, callback):
        """Watch the object returned by evaluating the identifier.

        Whenever I think the object might have changed, I'll send an
        ObjectLink of it to the callback.

        WARNING: This calls eval() on its argument!
        """
        object = eval(identifier,
                      self.globalNamespace,
                      self.localNamespace)
        return self.watchObject(object, identifier, callback)

    def watchObject(self, object, identifier, callback):
        """Watch the given object.

        Whenever I think the object might have changed, I'll send an
        ObjectLink of it to the callback.

        The identifier argument is used to generate identifiers for
        objects which are members of this one.
        """
        if type(object) is not types.InstanceType:
            raise TypeError, "Sorry, can only place a watch on Instances."

        # uninstallers = []

        dct = {}
        reflect.addMethodNamesToDict(object.__class__, dct, '')
        for k in object.__dict__.keys():
            dct[k] = 1

        members = dct.keys()

        clazzNS = {}
        clazz = types.ClassType('Watching%s%X' %
                                (object.__class__.__name__, id(object)),
                                (_MonkeysSetattrMixin, object.__class__,),
                                clazzNS)

        clazzNS['_watchEmitChanged'] = types.MethodType(
            lambda slf, i=identifier, b=self, cb=callback:
            cb(b.browseObject(slf, i)),
            None, clazz)

        # orig_class = object.__class__
        object.__class__ = clazz

        for name in members:
            m = getattr(object, name)
            # Only hook bound methods.
            if ((type(m) is types.MethodType)
                and (m.im_self is not None)):
                # What's the use of putting watch monkeys on methods
                # in addition to __setattr__?  Well, um, uh, if the
                # methods modify their attributes (i.e. add a key to
                # a dictionary) instead of [re]setting them, then
                # we wouldn't know about it unless we did this.
                # (Is that convincing?)

                monkey = _WatchMonkey(object)
                monkey.install(name)
                # uninstallers.append(monkey.uninstall)

        # XXX: This probably prevents these objects from ever having a
        # zero refcount.  Leak, Leak!
        ## self.watchUninstallers[object] = uninstallers


class _WatchMonkey:
    """I hang on a method and tell you what I see.

    TODO: Aya!  Now I just do browseObject all the time, but I could
    tell you what got called with what when and returning what.
    """
    oldMethod = None

    def __init__(self, instance):
        """Make a monkey to hang on this instance object.
        """
        self.instance = instance

    def install(self, methodIdentifier):
        """Install myself on my instance in place of this method.
        """
        oldMethod = getattr(self.instance, methodIdentifier, None)

        # XXX: this conditional probably isn't effective.
        if oldMethod is not self:
            # avoid triggering __setattr__
            self.instance.__dict__[methodIdentifier] = types.MethodType(
                self, self.instance, self.instance.__class__)
            self.oldMethod = (methodIdentifier, oldMethod)

    def uninstall(self):
        """Remove myself from this instance and restore the original method.

        (I hope.)
        """
        if self.oldMethod is None:
            return

        # XXX: This probably doesn't work if multiple monkies are hanging
        # on a method and they're not removed in order.
        if self.oldMethod[1] is None:
            delattr(self.instance, self.oldMethod[0])
        else:
            setattr(self.instance, self.oldMethod[0], self.oldMethod[1])

    def __call__(self, instance, *a, **kw):
        """Pretend to be the method I replaced, and ring the bell.
        """
        if self.oldMethod[1]:
            rval = apply(self.oldMethod[1], a, kw)
        else:
            rval = None

        instance._watchEmitChanged()
        return rval


class _MonkeysSetattrMixin:
    """A mix-in class providing __setattr__ for objects being watched.
    """
    def __setattr__(self, k, v):
        """Set the attribute and ring the bell.
        """
        if hasattr(self.__class__.__bases__[1], '__setattr__'):
            # Hack!  Using __bases__[1] is Bad, but since we created
            # this class, we can be reasonably sure it'll work.
            self.__class__.__bases__[1].__setattr__(self, k, v)
        else:
            self.__dict__[k] = v

        # XXX: Hey, waitasec, did someone just hang a new method on me?
        #  Do I need to put a monkey on it?

        self._watchEmitChanged()
