""" PyLint Panel for WingIde 2.1 Copyright (c) 2006-2007 Markus Meyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------------------------- Change Log ---------------------------------------------------------------------------- Version 1.0 (2006-10-12) * First release Version 1.1 (2007-03-19) * Fix compatibility issues with newer PyLint versions * Fix error message when no PyLint returns no errors """ PYLINTPANEL_VERSION = "1.1" import os import re import wingapi _AI = wingapi.CArgInfo # Scripts can be internationalized with gettext. Strings to be translated # are sent to _() as in the code below. #import gettext #_ = gettext.translation('scripts_pylintpanel', fallback = 1).gettext def _(a): return a # This special attribute is used so that the script manager can translate # also docstrings for the commands found here _i18n_module = 'scripts_pylintpanel' ###################################################################### # Utilities gMessageCategories = [ ("errors", _("Errors"), _("Errors that must be fixed")), ("warnings", _("Warnings"), _("Warnings that could indicate problems")), ("information", _("Info"), _("General informative messages")) ] gTheView = None # Will be set later ###################################################################### # Configuration file support class PylintConfig: def __init__(self): pass def get(self, name, default=""): """ Get configuration option from configuration file """ try: lines = file(self._get_config_file_name(), "rt").readlines() except IOError: return default for line in lines: words = line.split("=", 1) if len(words) == 2: key = words[0].strip() value = words[1].strip() if key == name: return value return default def edit(self): """ Edit and possibly create configuration file. Currently, this will just open the configuration file inside WingIDE. """ try: # Check if config file can be opened cfgfile = file(self._get_config_file_name(), "rt") except IOError: # The config file does not yet exist, create it cfgfile = file(self._get_config_file_name(), "wt") cfgfile.writelines([ '#\n', '# PyLint Panel Configuration\n', '#\n', '\n', '# Full path to PyLint executable\n', 'command = \n', '#command = /usr/bin/pylint\n', '#command = C:\Python24\Scripts\pylint.bat\n', '\n', '# Additional args to give to PyLint (may be blank)\n', 'args = \n', '#args = --disable-all --enable-variables=y --enable-miscellaneous=y --enable-imports=y --enable-classes=y\n', '\n', '# Timeout for execution of pylint command\n', 'timeout = 30\n', '\n', '# Save current file before running PyLint on it (1=yes, 0=no)\n', 'autosave = 1\n' ]) cfgfile.close() wingapi.gApplication.OpenEditor(self._get_config_file_name()) def _get_config_file_name(self): """ Get full name and path of config file """ dir = wingapi.gApplication.GetUserSettingsDir() return os.path.join(dir, "pylintpanel.cfg") gTheConfig = PylintConfig() ###################################################################### # Commands def pylint_preferences(): gTheConfig.edit() def ExecuteCommandLine(cmd, dirname, input, timeout, env): """Run the cmd synchronously in given directory and return its output. Input is any text to send to the sub-process (or None). Timeout defines max seconds to wait. The command is run in the environment that Wing started up in. Returns err, output_txt where err is one of: 0 -- Success 1 -- Command could not be launched 2 -- Command timed out Use AsyncExecuteCommandLine() to avoid locking up Wing while the command runs, or to access stderr or process exit status. """ from wingutils import spawn args = spawn.ParseCmdArgs(cmd) handler = spawn.CAsyncExecute(args[0], env, dirname, 10000.0, *args[1:]) if handler.terminated != None: # Assume command not launched, which I think is always the case handler.Terminate() return 1, '' if input: handler.pipes.tochild.write(input) import time max_time = time.time() + timeout finished = False while time.time() < max_time and not finished: finished = handler.Iterate() if not finished: time.sleep(0.1) if finished: errno = 0 else: errno = 2 stdout, stderr, err, exit_status = handler.Terminate(kill=True) return (errno, stdout) def execute_pylint(): if gTheView is None: # Panel is not visible return view = gTheView app = wingapi.gApplication pylint_command = gTheConfig.get("command", None) pylint_args = gTheConfig.get("args", "") pylint_timeout = gTheConfig.get("timeout", "10000") pylint_autosave = gTheConfig.get("autosave", "1") if pylint_command is None: app.ShowMessageDialog(_('Error'), _('PyLint panel configuration file not found. ' + 'Choose "Preferences" from the context menu to edit ' + 'the configuration file.')) return try: timeout = int(pylint_timeout) autosave = int(pylint_autosave) != 0 except ValueError: app.ShowMessageDialog(_("Error"), _("Invalid values specified in configuration file")) return filename = app.GetActiveEditor().GetDocument().GetFilename() view._lastFilename = filename cmd = '"%s" "%s" --reports=n --include-ids=yes %s' % ( pylint_command, filename, pylint_args) # Save active document before executing PyLint if autosave: app.ExecuteCommand("save") # Execute PyLint env = config.gStartupEnv.copy() from proj import project from wingutils import location env['PYTHONPATH'] = project.GetEffectiveValue(project.attribs.kPyPath, location.GetLocalFileLocation(filename)) err, result = ExecuteCommandLine(cmd, os.path.split(filename)[0], None, timeout, env) if err == 2: app.ShowMessageDialog(_("Error"), _("Error executing PyLint:\n\nCommand timed out")) return if err == 1: app.ShowMessageDialog(_("Error"), _("Error executing PyLint:\n\nCommand could not be launched")) return compiled_expr = re.compile("([^:]+):[ ]*([0-9]+):(.*)") resultLines = result.split('\n') tree_contents = [ [], [], [] ] for line in resultLines: matchobj = compiled_expr.match(line) if matchobj is not None: msg_type = matchobj.group(1) msg_line = matchobj.group(2).strip() msg_descr = matchobj.group(3).strip() if msg_type[0] == 'F' or msg_type[0] == 'E': msg_index = 0 elif msg_type[0] == 'W': msg_index = 1 else: msg_index = 2 tree_contents[msg_index].append( ((msg_line, msg_type + ": " + msg_descr),)) view.set_tree_contents(tree_contents) ###################################################################### # XXX This is advanced scripting that accesses Wing IDE internals, which # XXX are subject to change from version to version without notice. from guiutils import wgtk from guiutils import dockview from guiutils import wingview from guiutils import winmgr from command import commandmgr import guimgr.menus # Note that panel IDs must be globally unique so all user-provided panels # MUST add a random uniquifier after '#'. The panel can still be referred # to by the portion of the name before '#' and Wing will warn when there # are multiple panel definitions with the same base name (in which case # Wing-defined panels win over user-defined panels and otherwise the # last user-defined panel type wins when referred to w/o the uniquifier). _kPanelID = 'pylintpanel#02EFWRQK9X23' class _CPylintPanelDefn(dockview.CPanelDefn): """Panel definition for the project manager""" def __init__(self, singletons): self.fSingletons = singletons dockview.CPanelDefn.__init__(self, self.fSingletons.fPanelMgr, _kPanelID, 'tall', 0) winmgr.CWindowConfig(self.fSingletons.fWinMgr, 'panel:%s' % _kPanelID, size=(350, 1000)) def _CreateView(self): return _CPylintView(self.fSingletons) def _GetLabel(self, panel_instance): """Get display label to use for the given panel instance""" return _('PyLint') def _GetTitle(self, panel_instance): """Get full title for the given panel instance""" return _('PyLint Panel') class _CPylintViewCommands(commandmgr.CClassCommandMap): def __init__(self, singletons, view): commandmgr.CClassCommandMap.__init__(self, domain='user', package='pylintpanel', i18n_module=_i18n_module) assert isinstance(view, _CPylintView) self.fSingletons = singletons self.__fView = view class _CPylintView(wingview.CViewController): """A single template manager view""" def __init__(self, singletons): """ Constructor """ global gTheView # Init inherited wingview.CViewController.__init__(self, ()) # External managers self.fSingletons = singletons self.__fCmdMap = _CPylintViewCommands(self.fSingletons, self) self.fTrees = {} self.fLabels = {} self.__CreateGui() # Remember that this is the default view now gTheView = self def _destroy_impl(self): for tree, sview in self.fTrees.values(): sview.destroy() def set_tree_contents(self, tree_contents): idx = 0 for catkey, labeltext, tooltip in gMessageCategories: label = gTheView.fLabels[catkey] label.set_text('%s (%i)' % (labeltext, len(tree_contents[idx]))) tree, sview = gTheView.fTrees[catkey] tree.set_contents(tree_contents[idx]) idx += 1 ########################################################################## # Inherited calls from wingview.CViewController ########################################################################## def GetDisplayTitle(self): """ Returns the title of this view suitable for display. """ return _("PyLint Panel") def GetCommandMap(self): """ Get the command map object for this view. """ return self.__fCmdMap def BecomeActive(self): pass ########################################################################## # Popup menu and actions ########################################################################## def __CreateGui(self): notebook = wgtk.Notebook() for catkey, label, tooltip in gMessageCategories: tree = wgtk.SimpleTree([wgtk.gobject.TYPE_STRING] * 2, [wgtk.CellRendererText(), wgtk.CellRendererText()], [_("Line"), _("Message")]) tree.unset_flags(wgtk.CAN_FOCUS) tree.set_property('headers-visible', True) tree.connect('button-press-event', self.__CB_ButtonPress) sel = tree.get_selection() sel.connect('changed', self.__CB_SelectionChanged) tree.show() sview = wgtk.ScrolledWindow() sview.set_policy(wgtk.POLICY_AUTOMATIC, wgtk.POLICY_AUTOMATIC) sview.add(tree) sview.show() # Event box is needed to make tooltips work in this context (don't ask) tab_event_box = wgtk.TooltipBox() tab_label = wgtk.Label(label) tab_event_box.add(tab_label) tab_event_box.show_all() wgtk.set_tooltip(tab_event_box, tooltip) notebook.append_page(sview, tab_event_box) self.__fNotebook = notebook self.fTrees[catkey] = (tree, sview) self.fLabels[catkey] = tab_label notebook.set_current_page(0) self._SetGtkWidget(notebook) def __CreatePopup(self): """Construct popup menu for this object.""" kPopupDefn = [ ( _("Update"), 'execute-pylint' ), ( _("Preferences..."), 'pylint-preferences' ) ] # Create menu defnlist = guimgr.menus.GetMenuDefnList(kPopupDefn, self.fSingletons.fGuiMgr, self.__fCmdMap, is_popup=1, static=1) menu = guimgr.menus.CMenu(_("PyLint"), self.fSingletons.fGuiMgr, defnlist, can_tearoff=0, is_popup=1) return menu def __CB_SelectionChanged(self, sel): pos = self.__fNotebook.get_current_page() catkey, label, tdir = gMessageCategories[pos] tree, sview = self.fTrees[catkey] rows = tree.GetSelectedContent() def __CB_ButtonPress(self, tree, event): app = wingapi.gApplication # Always select the row that the pointer is over event_path_info = tree.get_path_at_pos(int(event.x), int(event.y)) if event_path_info is not None: event_path = event_path_info[0] selected_paths = tree.GetSelectedPaths() if event_path not in selected_paths: sel = tree.get_selection() sel.unselect_all() sel.select_path(event_path) # Popup menu on right mouse button if event.button == 3: self.__PopupMenu(event, (event.x_root, event.y_root)) return 1 selected = tree.GetSelectedContent() if selected is not None and len(selected) != 0: line = int(selected[0][0]) if event.button == 1 and event.type == wgtk.gdk.BUTTON_PRESS: doc = app.OpenEditor(self._lastFilename) doc.ScrollToLine(lineno=line-1, pos='center', select=1) def __PopupMenu(self, event, pos): """Callback to display the popup menu""" menu = self.__CreatePopup() menu.Popup(event, pos=pos) # Register this panel type: Note that this needs to be at the # very end of the module so that all the classes defined here # are already available import config if _kPanelID not in config.kDefaultPanels: config.kDefaultPanels.append(_kPanelID) _CPylintPanelDefn(wingapi.gApplication.fSingletons)