# -*- coding: utf-8 -*-
#
# Copyright © 2011 Pierre Raybaut
# Licensed under the terms of the MIT License
# (see spyderlib/__init__.py for details)

"""
Patching rope:

[1] For compatibility with Spyder's standalone version, build with py2exe or
    cx_Freeze

[2] For better performances, see this thread:
http://groups.google.com/group/rope-dev/browse_thread/thread/57de5731f202537a

[3] To avoid considering folders without __init__.py as Python packages, thus
avoiding side effects as non-working introspection features on a Python module
or package when a folder in current directory has the same name.
See this thread:
http://groups.google.com/group/rope-dev/browse_thread/thread/924c4b5a6268e618

[4] To avoid rope adding a 2 spaces indent to every docstring it gets, because
it breaks the work of Sphinx on the Object Inspector.
"""

def apply():
    """Monkey patching rope
    
    See [1], [2] and [3] in module docstring."""
    import rope
    if rope.VERSION not in ('0.9.4', '0.9.3', '0.9.2'):
        raise ImportError, "rope %s can't be patched" % rope.VERSION

    # [1] Patching project.Project for compatibility with py2exe/cx_Freeze
    #     distributions
    from spyderlib.baseconfig import is_py2exe_or_cx_Freeze
    if is_py2exe_or_cx_Freeze():
        from rope.base import project
        class PatchedProject(project.Project):
            def _default_config(self):
                # py2exe/cx_Freeze distribution
                from spyderlib.baseconfig import get_module_source_path
                fname = get_module_source_path('spyderlib',
                                               'default_config.py')
                return open(fname, 'rb').read()
        project.Project = PatchedProject
    
    # Patching pycore.PyCore...
    from rope.base import pycore
    class PatchedPyCore(pycore.PyCore):
        # [2] ...so that forced builtin modules (i.e. modules that were 
        # declared as 'extension_modules' in rope preferences) will be indeed
        # recognized as builtins by rope, as expected
        # 
        # This patch is included in rope 0.9.4+ but applying it anyway is ok
        def get_module(self, name, folder=None):
            """Returns a `PyObject` if the module was found."""
            # check if this is a builtin module
            pymod = self._builtin_module(name)
            if pymod is not None:
                return pymod
            module = self.find_module(name, folder)
            if module is None:
                raise pycore.ModuleNotFoundError(
                                            'Module %s not found' % name)
            return self.resource_to_pyobject(module)
        # [3] ...to avoid considering folders without __init__.py as Python
        # packages
        def _find_module_in_folder(self, folder, modname):
            module = folder
            packages = modname.split('.')
            for pkg in packages[:-1]:
                if  module.is_folder() and module.has_child(pkg):
                    module = module.get_child(pkg)
                else:
                    return None
            if module.is_folder():
                if module.has_child(packages[-1]) and \
                   module.get_child(packages[-1]).is_folder() and \
                   module.get_child(packages[-1]).has_child('__init__.py'):
                    return module.get_child(packages[-1])
                elif module.has_child(packages[-1] + '.py') and \
                     not module.get_child(packages[-1] + '.py').is_folder():
                    return module.get_child(packages[-1] + '.py')
    pycore.PyCore = PatchedPyCore
    
    # [2] Patching BuiltinFunction for the calltip/doc functions to be 
    # able to retrieve the function signatures with forced builtins
    from rope.base import builtins, pyobjects
    from spyderlib.utils.dochelpers import getargs
    class PatchedBuiltinFunction(builtins.BuiltinFunction):
        def __init__(self, returned=None, function=None, builtin=None,
                     argnames=[], parent=None):
            builtins._BuiltinElement.__init__(self, builtin, parent)
            pyobjects.AbstractFunction.__init__(self)
            self.argnames = argnames
            if not argnames and builtin:
                self.argnames = getargs(self.builtin)
            if self.argnames is None:
                self.argnames = []
            self.returned = returned
            self.function = function
    builtins.BuiltinFunction = PatchedBuiltinFunction

    # [2] Patching BuiltinName for the go to definition feature to simply work 
    # with forced builtins
    from rope.base import libutils
    import inspect
    class PatchedBuiltinName(builtins.BuiltinName):
        def _pycore(self):
            p = self.pyobject
            while p.parent is not None:
                p = p.parent
            if isinstance(p, builtins.BuiltinModule) and p.pycore is not None:
                return p.pycore
        def get_definition_location(self):
            if not inspect.isbuiltin(self.pyobject):
                _lines, lineno = inspect.getsourcelines(self.pyobject.builtin)
                path = inspect.getfile(self.pyobject.builtin)
                pycore = self._pycore()
                if pycore and pycore.project:
                    resource = libutils.path_to_resource(pycore.project, path)
                    module = pyobjects.PyModule(pycore, None, resource)
                    return (module, lineno)
            return (None, None)
    builtins.BuiltinName = PatchedBuiltinName
    
    # [4] Patching PyDocExtractor so that _get_class_docstring and
    # _get_single_function_docstring don't add a 2 spaces indent to
    # every docstring. The only value that we are modifying is the indent
    # keyword, from 2 to 0.
    from rope.contrib import codeassist
    class PatchedPyDocExtractor(codeassist.PyDocExtractor):
        def _get_class_docstring(self, pyclass):
            contents = self._trim_docstring(pyclass.get_doc(), indents=0)
            supers = [super.get_name() for super in pyclass.get_superclasses()]
            doc = 'class %s(%s):\n\n' % (pyclass.get_name(), ', '.join(supers)) + contents

            if '__init__' in pyclass:
                init = pyclass['__init__'].get_object()
                if isinstance(init, pyobjects.AbstractFunction):
                    doc += '\n\n' + self._get_single_function_docstring(init)
            return doc
            
        def _get_single_function_docstring(self, pyfunction):
            signature = self._get_function_signature(pyfunction)
            docs = pyfunction.get_doc()
            docs = self._trim_docstring(pyfunction.get_doc(), indents=0)
            return docs
            return signature + ':\n\n' + docs
    codeassist.PyDocExtractor = PatchedPyDocExtractor 
