plugins/python.py
author root
Wed, 26 Apr 2006 10:48:09 +0000
changeset 0 7710b138d4eb
permissions -rw-r--r--
forget the past. forget the past.

# Copyright (c) 2004 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Basic plugin for python modules
"""

__revision__ = '$Id: python.py,v 1.25 2006-03-26 20:19:31 nico Exp $'

import os
from os.path import exists, join

import gtk, gobject

from pigg.form import PyFormController
from pigg.mvc import VSensitivity

from logilab import astng
from logilab.common.configuration import OptionsManager2ConfigurationAdapter

from pylint.lint import PyLinter
from pylint.reporters import BaseReporter
from pylint import checkers    
    
from oobrother.plugins import AbstractPlugIn
from oobrother.sysutils import OOBROTHER_HOME, PathNormalizerMixIn, call_and_restore
from oobrother.uiutils import init_treeview_columns, DelegatedWindowMixIn, \
    TreeViewMixIn, load_pixbuf
from oobrother.uiutils.windows import ConfigurableListWindow
from oobrother.config_tools import ConfigurationModel, notebook_from_config, \
     format_config_entry, PluggableConfig, ConfigurableNode

__metaclass__ = type
    

PYLINT_MESSAGE_LEVEL = {
    'E' : 1,
    'W' : 2,
    'R' : 3,
    'C' : 4,
    'I' : 5,
    'F' : 6,
    }

def compare_message(sigle1, sigle2):
    """compares severity of two pylint messages"""
    sigle1_level = PYLINT_MESSAGE_LEVEL.get(sigle1, -1)
    sigle2_level = PYLINT_MESSAGE_LEVEL.get(sigle2, -1)
    if sigle1_level == -1:
        return -1
    if sigle2_level == -1:
        return 1
    return cmp(sigle1_level, sigle2_level)

def row_sort(model, iter1, iter2, data):
    """Sort method for pylint report table"""
    sigle1 = model.get_value(iter1, 3)
    sigle2 = model.get_value(iter2, 3)
    return compare_message(sigle1, sigle2)    
    
def display_name(node):
    """return a display string for an astng node (may be a package,
    module, class or function)
    """
    if isinstance(node, astng.Class):
        base = node.basenames and '(%s)' % ', '.join(node.basenames) or ''
        return node.name + base
    if isinstance(node, astng.Function):
        return '%s(%s)' % (node.name, node.format_args())
    #if isinstance(node, astng.Module) or isinstance(node, astng.Package):
    return node.name

def schema_from_options_manager(opt_manager):
    """return a pigg.mvc compatible schema created from options registered in
    the given logilab.common.configuration.OptionsManager instance
    """
    schema = []
    for provider in opt_manager.options_providers:
        default_options = []
        sections = {}
        for opt_name, opt_dict in provider.options:
            if opt_dict.get('type') is None:
                continue                
            attr_def = format_config_entry(opt_name, opt_dict)
            schema.append(attr_def)
    return schema

        
class PylintPluggableConfig(OptionsManager2ConfigurationAdapter):
    """pylint specific pluggable config"""
    
    def __init__(self, provider, model, ctrl):
        OptionsManager2ConfigurationAdapter.__init__(self, provider)
        self.model, self.ctrl = model, ctrl
        form = notebook_from_config(provider, model, ctrl)
        self.wdg = form.wdg
        self.wdg.show_all()
        # FIXME: should probably be in another place
        if provider.name != 'master':
            key = 'enable-%s' % provider.name
            for opt_name, opt_dict in provider.options:
                if opt_dict.get('type') is None:
                    continue
                if opt_name == key:
                    continue
                try:
                    VSensitivity(form[opt_name], model, ctrl, key)
                except KeyError:
                    continue


# plugin's windows ############################################################

class PylintReport(PathNormalizerMixIn, BaseReporter, ConfigurableListWindow):
    """A simple table for pylint reports"""
    name = 'pylint'
    
    def __init__(self):
        BaseReporter.__init__(self)
        ConfigurableListWindow.__init__(self, 'pylint_display.ini',
                                        (gobject.TYPE_STRING, # msg_type
                                         gobject.TYPE_STRING, # file
                                         gobject.TYPE_INT, # line
                                         gobject.TYPE_STRING, # message
                                         gobject.TYPE_STRING, # obj
                                         gobject.TYPE_STRING, # color
                                         ),
                                        ('Type', 'File', 'Line',
                                         'Object', 'Message'),
                                        )
        self.editor = None # will be set later, I promise
        self.init_columns(self.cfg['columns'],
                          (0, 0, 0, 0, 4), foreground=5)
        store = self.treeview.get_model()
        store.set_sort_func(4, row_sort)
        self.win.set_default_size(600, 400)
        # pylint initialization
        self.pylint = pylint = PyLinter()
        checkers.initialize(pylint)
        pylint.config_file = join(OOBROTHER_HOME, 'pylint.ini')
        pylint.load_file_configuration()
        pylint.set_reporter(self)
        pylint.set_option('reports', False)
        # remove "report" options from options definition
        options = [(o_name, o_dict) for o_name, o_dict in pylint.options
                   if o_dict.get('group') != 'Reports']
        pylint.options = options
        # pylint's configuration objects
        model = ConfigurationModel(OptionsManager2ConfigurationAdapter(pylint),
                                   schema_from_options_manager(pylint))
        model.update()
        ctrl = PyFormController(model)
        self._subcfgs = [ConfigurableNode(prov.name,
                                          PylintPluggableConfig(prov, model, ctrl))
                         for prov in pylint.options_providers]
        self.treeview.connect('button-press-event', self.cb_button_pressed)
        
    def check(self, thefile):
        """check the given wrapped file using pylint"""
        # FIXME: pylint should be able to use an existent astng implementation
        #astng = self._get_astng(thefile)
        self.clear()
        self.set_title('PyLint: (%s)' % thefile.abspath)
        self.show()
        self.set_base(thefile)
        #call_and_restore(self.pylint.check, thefile.abspath)
        self.pylint.check(thefile.abspath)

    # IConfigurable
    
    def subconfiguration(self):
        """return a list of configuration nodes for subconfiguration
        (i.e. pylint's checkers)        
        """
        return self._subcfgs

    # gui callbacks

    def cb_button_pressed(self, treeview, event):
        """open file on the corresponding line on double click
        """
        # left click
        if event.button == 1:
            # double_click
            if event.type == gtk.gdk._2BUTTON_PRESS:
                _, file_path, lineno, _, _, _ = self.get_selected()
                self.editor.open(self.absolute_path(file_path), lineno)
        
    # pylint reporter interface
    
    def add_message(self, msg_id, location, msg):
        """get pylint messages"""
        # self.win.freeze_notify()
        path, module, obj, line = location
        if msg_id[0] == 'W':
            color = 'blue'
        elif msg_id[0] == 'E':
            color = 'red'
        else:
            color = 'black'
        self.append_line((msg_id, self.normalize_path(path), line, str(obj),
                          msg, color))
                
    def display_results(self, layout):
        """asked to display the pylint reports"""
        print 'DISPLAYING PYLINT REPORTS'


PYOBJECTS_PIXBUFS = {
    'Class'    : load_pixbuf('class.png', 20),
    'Module'   : load_pixbuf('class.png', 20),
    'Method'   : load_pixbuf('class.png', 20),
    'Function' : load_pixbuf('class.png', 20),
    'Global'   : load_pixbuf('class.png', 20),
    }

def pyobject_pixbuf(column, cell, model, iter):
    pyobj = model.get_value(iter, 2)
    pix = PYOBJECTS_PIXBUFS.get(pyobj.__class__.__name__, None)
    cell.set_property('pixbuf', pix)

def pyobject_name(column, cell, model, iter):
    pyobj_name = model.get_value(iter, 0)
    cell.set_property('text', pyobj_name)

class ModulesBrowser(DelegatedWindowMixIn, TreeViewMixIn):
    """Python Objects Browser, using the gtk tree view"""
    name = 'objects browser'
    options = (('hide-private', {'type' : 'yn', 'default': True}),
               )
    
    def __init__(self):
        # We need to have self.treeview in TreeviewMixIn's init()
        super(ModulesBrowser, self).__init__()
        self.win.set_default_size(200, 300)
        self.set_title('Python objects browser')
        self.cfg = PluggableConfig(self.name, 'pyobrowser.ini', self.options)
        self.editor = None # will be set later, I promise
        scr = gtk.ScrolledWindow()
        scr.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.win.add(scr)
        self._store = gtk.TreeStore(gobject.TYPE_STRING, # obj name
                                    gobject.TYPE_INT,    # line number
                                    gobject.TYPE_PYOBJECT, # the astng node
                                    )
        self.treeview = gtk.TreeView(self._store)
        self.activate_mouse_callbacks()
        # init columns / set model
        # init_treeview_columns(self.treeview, ['Module objects'])
        pixbuf_renderer = gtk.CellRendererPixbuf()
        col = gtk.TreeViewColumn('Module objects', pixbuf_renderer)
        col.set_cell_data_func(pixbuf_renderer, pyobject_pixbuf)
        text_renderer = gtk.CellRendererText()
        col.pack_start(text_renderer, False)
        col.set_cell_data_func(text_renderer, pyobject_name)
        self.treeview.append_column(col)
        scr.add(self.treeview)
        self.win.connect('delete-event', self.hide)

    def subconfiguration(self):
        return []
    
    def add_object(self, thefile):
        """add a python file to the module browser"""
        store = self._store
        node = thefile.astng
        mod_iter = store.append(None, (display_name(node), 0, node))
        self.add_subobjects(mod_iter, node)

    def add_subobjects(self, store_iter, node):
        """fill the treemodel with package/module/class/method/function
        informations
        """
        #print 'add subobjects of', node.__class__.__name__, node.name
        hide_priv = self.cfg['hide-private']
        for name in node:
            if hide_priv and name.startswith('_'):
                continue
            cnode = node[name]
            new_iter = self._store.append(store_iter, (display_name(cnode), cnode.lineno or 0, cnode))
            self.add_subobjects(new_iter, cnode)


    def on_double_click(self, row_model, event):
        """called on double-click"""
        obj_name, lineno, astng = row_model
        if lineno != -1:
            filepath = astng.root().file
            self.editor.open(filepath, lineno)

        return gtk.TRUE
    
##     def oob_button_pressed(self, treeview, event):
##         """Callback called when the user clicks on the FS treeview"""
##         # left click
##         if event.button == 1:
##             # double_click
##             if event.type == gtk.gdk._2BUTTON_PRESS:
##                 obj_name, lineno, astng = self.get_selected()
##                 if lineno != -1:
##                     filepath = astng.root().file
##                     self.editor.open(filepath, lineno)


# the plugin itself ###########################################################

class PythonPlugin(AbstractPlugIn):
    """Plugin for python files"""
    name = 'python'
    mimetypes = 'text/x-python',

    def __init__(self):
        AbstractPlugIn.__init__(self)
        self.pylint_window = PylintReport()
        try:
            self.browser_window = ModulesBrowser()
        except:
            import traceback
            traceback.print_exc()
        self.astng_manager = astng.ASTNGManager()
        
    def set_application(self, appl):
        """set the application (on registration)"""
        AbstractPlugIn.set_application(self, appl)
        self.pylint_window.editor = self.appl.editor
        self.browser_window.editor = self.appl.editor
        
    def support_directory(self, thedir):
        """return true if the directory is a python package"""
        if exists(join(thedir.abspath, '__init__.py')):
            return True
        return False
    
    def get_actions(self, thefile):
        """return actions provided by this plugin for the given file (python
        module) or directory (python package)
        """
        if thefile.is_directory():
            # python package
            actions = [
                ('lint', self.cb_lint),
                ('add to objects browser', self.cb_add_to_browser),
                ]
            return actions
        # python module
        return [
            ('execute', self.cb_execute),
            ('lint', self.cb_lint),
            ('add to objects browser', self.cb_add_to_browser),
            ]

    def cb_execute(self, menuitem, thefile):
        """execute a python file, output to the shared console"""
        self.execute_in_console('python %s' % thefile.abspath)        

    def cb_lint(self, menuitem, thefile):
        """lint a python module or package"""
        self.pylint_window.check(thefile)
    
    def cb_add_to_browser(self, menuitem, thefile):
        """a a python module or package to the python objects browser"""
        self.browser_window.show()
        self._get_astng(thefile)
        self.browser_window.add_object(thefile)
        
    def _get_astng(self, thefile):
        """return the astng representation for this object (cached or new build)
        """
        if not hasattr(thefile, 'astng'):
            if thefile.is_directory():
                thefile.astng = self.astng_manager.from_directory(thefile.abspath)
            else:
                thefile.astng = self.astng_manager.from_file(thefile.abspath)
        return thefile.astng
    
    def subconfiguration(self):
        return [self.pylint_window, self.browser_window]

        
def register(registry):
    """register plugins from this module to the plugins registry"""
    registry.register(PythonPlugin())