plugins/devtools.py
author Nicolas Chauvat <nicolas.chauvat@logilab.fr>
Mon, 23 Jun 2008 13:42:22 +0200
changeset 6 6aad8b499e86
parent 0 7710b138d4eb
permissions -rw-r--r--
fix reST markup

# pylint: disable-msg=C0301
# 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 providing a gui for the logilab.devtools package
"""

__revision__ = '$Id: devtools.py,v 1.35 2005-02-25 17:25:12 adim Exp $'

import sys
import os
import glob
import time
import shutil
import tempfile
from os.path import exists, join, split, basename, dirname

import gtk, gobject
from pigg import form, wgenerator, utils

from stat import S_IWRITE
from logilab.common.fileutils import ensure_fs_mode
from logilab.common.modutils import load_module_from_name
from logilab.common.textutils import normalize_text

from logilab.devtools import TEMPLATE_DIR, lib, check_package, buildeb, \
     debianize
from logilab.devtools.lib import coverage, changelog, pkginfo
try:
    from logilab.devtools.lib import pkghandlers
except:
    from logilab.devtools import debhdlrs as pkghandlers
    
from oobrother.plugins import AbstractPlugIn
from oobrother.sysutils import PathNormalizerMixIn, call_in_directory, \
     call_and_restore
from oobrother.uiutils import WindowMixIn, ask_for_message
from oobrother.uiutils.windows import ConfigurableListWindow, \
     SystemCommandWindow
from oobrother.config_tools import PluggableConfig

__metaclass__ = type

# FIXME : ChangeLogModel was using devtools.lib.changelog.TODAY wich doesn't
#         exist. TODAY should go elsewhere (logilab.common.utils.today() ?)
TODAY = time.strftime("%Y/%m/%d", time.localtime(time.time()))

# FIXME: rewrite this function to use the schema in the pkginfo module
# and move it there

def pkginfo_create(schema, pkginfo_file, data):
    """create a __pkginfo__file"""
    stream = open(pkginfo_file, 'w')
    # insert license
    license = data['license']
    license_text = normalize_text(pkginfo.get_license_text(license), indent='# ')
    stream.write(license_text + '\n')
    # insert docstring
    stream.write('"""package information for %s"""\n\n' % data['modname'])
    # insert revision string
    stream.write("__revision__ = '$I"+"d:$'\n\n")
    # insert configuration
    for key, wtype, wlabel, wargs, required, default in schema:
        value = data.get(key)
        if value is None:
            #if required:
            #    log(LOG_ERR, 'missing required value for %r' % key)
            #else:
            log(LOG_INFO, 'skipping None value for %r', key)
            continue
        stream.write('%s = %r\n' % (key, value))
    stream.write("numversion = [int(v) for v in version.split('.')]\n")
    stream.close()

DEB_PKG_HANDLERS = pkghandlers.PKG_HANDLERS.keys()
DEB_PKG_HANDLERS.sort()
    

README_TEMPLATE = """%s
%s


What's this ?
-------------
%s


Installation
------------

Extract the tarball, jump into the created directory and run ::

	python setup.py install

For installation options, see ::

	python setup.py install --help

License
-------
%s
"""


def is_root_package(path):
    """return true if path is not part of an upper package"""
    return not exists(join(split(path)[0], '__init__.py'))


COLORS = {
    lib.INFO : 'black',
    lib.WARNING : 'blue',
    lib.ERROR : 'red',
    lib.FATAL : 'red'
    }

class DevtoolsLogWindow(PathNormalizerMixIn, ConfigurableListWindow):
        
    def __init__(self, config_file, name=None):
        self.name = name or self.name
        ConfigurableListWindow.__init__(self, config_file,
                                        (gobject.TYPE_STRING, # severity
                                         gobject.TYPE_STRING, # file path
                                         gobject.TYPE_INT, # line no
                                         gobject.TYPE_STRING, # message
                                         gobject.TYPE_STRING, # color
                                         ),
                                        ('Severity', 'Path', 'Line', 'Message'),
                                        )
        self.win.set_default_size(500, 200)
        self.editor = None # will be set later, I promise
        self.init_columns(self.cfg['columns'], (0, 0, 0, 0), foreground=4)
        self.treeview.connect('button-press-event', self.cb_button_pressed)
        
    def log(self, severity, path, line, msg):
        """log a message of a given severity
        
        line may be None if unknown
        """
        path = self.normalize_path(path)
        self.append_line( (lib.REVERSE_SEVERITIES[severity], path, line or 0,
                           msg, COLORS[severity]) )

    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)

    
# pkg info handling ###########################################################

class PkgInfoModel(form.PyFormModel):
    """a pkginfo specific model"""
    
    def __init__(self, pkginfo_file):
        schema = (
            ('modname', 'string', 'name', (), True, self.default_modname), 
            ('version', 'string', 'version', (), True, '0.1'),
            ('copyright', 'text', 'copyright', (), True,
'''Copyright (c) %s LOGILAB S.A. (Paris, FRANCE).
http://www.logilab.fr/ -- mailto:contact@logilab.fr''' % time.localtime()[0]),
            ('license', 'choice', 'license', (pkginfo.get_known_licenses(),),
             False, 'GPL'),
            ('subpackage_of', 'string', 'sub-package of', (), False, None),
            ('pyversions', 'multiple_choice', 'supported python versions',
             (('2.1', '2.2', '2.3'),), False, None),

            ('short_desc', 'string', 'short description', (), True, ''),
            ('long_desc', 'text', 'long description', (), True, ''),

            ('author', 'string', 'author name', (), True, 'Logilab'),
            ('author_email', 'string', 'author email', (), True, 'devel@logilab.fr'),
            ('web', 'string', 'home page', (), False, self.default_homepage),
            ('ftp', 'string', 'ftp download directory', (), False, self.default_ftp),
            ('mailinglist', 'string', 'mailing list', (), False, None),
            ('debian_maintainer', 'string', 'debian maintainer name', (), False, None),
            ('debian_maintainer_email', 'string', 'debian maintainer email', (), False, None),
            ('debian_handler', 'choice', 'debian handler', (DEB_PKG_HANDLERS,), False, None),
            )
        self.file = pkginfo_file
        form.PyFormModel.__init__(self, schema, {})
        base = split(pkginfo_file)[0]
        # initialize model with data from the __pkginfo__ module if it exists
        if pkginfo_file is not None and exists(pkginfo_file):
            # FIXME: ugly hack to avoid importing the oobrother's __pkginfo__ that
            # is registered in sys.modules
            sys.path.insert(0, base)
            module = call_in_directory(base, load_module_from_name, '__pkginfo__')
            sys.path.pop(0)
            values = {}
            for attr in self.schema:
                attr = attr[0]
                try:
                    values[attr] = getattr(module, attr)
                except AttributeError:
                    log(LOG_INFO, '__pkginfo__ has no attribute %s', attr)
                    continue
            self.set_values(values, True)

    def default_modname(self):
        """get the default value of the 'modname' field"""
        return basename(dirname(self.file))
    
    def default_homepage(self):
        """get the default value of the 'homepage' field"""
        return 'http://www.logilab.org/projects/%s' % (self._data.get('modname') or
                                                       self.default_modname())
    def default_ftp(self):
        """get the default value of the 'ftp' field"""
        return 'ftp://ftp.logilab.org/pub/%s' % (self._data.get('modname') or
                                                 self.default_modname())
    
    def _commit(self):
        """write back the model's content (the form is completed at this point)
        """
        pkfinfo = self.file
        if not exists(pkfinfo):
            # FIXME: notify FS model
            pkginfo_create(self.schema, pkfinfo, self.as_dict())
            log(LOG_INFO, '__pkginfo__.py created')
        else:
            modifs = self.get_modifications()
            if modifs:
                pkginfo.pkginfo_save(pkfinfo, modifs)
                log(LOG_INFO, '__pkginfo__.py saved')
            else:
                log(LOG_INFO, 'no modification to save to __pkginfo__.py')



class ModalFormWindow(WindowMixIn):
    def __init__(self, title, model):
        super(ModalFormWindow, self).__init__()
        self.model = model
        self.ctrl = form.PyFormController(model)
        self.win = gtk.Dialog(title, None, 0,
                         (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT,
                          gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
        self.win.set_default_size(400, 500)
        
    def run(self):
        """open a window to edit __pkginfo__ fields"""
        result = self.win.run()
        if result == gtk.RESPONSE_ACCEPT:
            try:
                self.model.commit()
            except form.IncompleteForm:
                utils.show_msg('The form is not complete. Bold fields are required.',
                               gtk.MESSAGE_ERROR)
                return self.run()
        elif self.model.get_modifications():
            if utils.confirm('There are some unsaved modifications. Close anyway ?'):
                # FIXME: restore original values ?
                self.model.rollback()
            else:
                return self.run()                
        self.hide()
        return result


class PkgInfoWindow(ModalFormWindow):
    """a window to edit __pkginfo__.py file"""
    
    def __init__(self, pkginfo_file):
        super(PkgInfoWindow, self).__init__(pkginfo_file,
                                            PkgInfoModel(pkginfo_file))
        schema = self.model.schema
        nb_desc = (
            ('base', schema[0:6]),
            ('description', schema[6:8]),
            ('miscellaneous', schema[8:13]),
            ('debian', schema[13:]),
            )
        wdg = wgenerator.notebook_generate(nb_desc, self.model, self.ctrl)
        self.win.vbox.pack_end(wdg.wdg)
        self.win.show_all()
        # FIXME: why do I have to call notify to set form values according to the model ??
        self.model.notify_all()


# ChangeLog handling ##########################################################

class ChangeLogModel(form.PyFormModel):
    """a changelog specific model"""
    cl_class = changelog.ChangeLog
    
    def __init__(self, thedir, close=False, schema=None, filename='ChangeLog'):
        schema = schema or (
            ('version', 'string', 'version', (), False, self.default_version), 
            ('date', 'string', 'date', (), False, self.default_date), 
            ('entries', 'text', 'entries', (), True, self.default_entries), 
            )
        self.file = join(thedir.abspath, filename)
        self.thedir = thedir
        form.PyFormModel.__init__(self, schema, {})
        if close:
            self.set_values({'version': changelog.get_pkg_version(self.thedir.abspath),
                             'date': TODAY})
            
    def default_version(self):
        """get the current change log version"""        
        return self._get_entry().version

    def default_date(self):
        """get the current change log version"""
        return self._get_entry().date

    def default_entries(self):
        """get the default change log entries"""        
        stream = []
        for msg in self._get_entry().messages:
            stream.append('%s %s' % (changelog.BULLET, msg[0]))
            for more in msg[1:]:
                stream.append('  %s' % (more))
            stream.append('')
        return '\n'.join(stream)
    
    def update_entry(self):
        """update the associated changelog's entry according to current
        model values
        """
        entry = self._entry
        entry.version = self.get_value('version')
        entry.date = self.get_value('date')
        entry.messages = []
        data = self.get_value('entries')
        try:
            for line in data.splitlines():
                line = line.strip()
                if not line :
                    continue
                if line[0] == changelog.BULLET:
                    entry.add_message(line[1:].lstrip())
                else:
                    entry.complete_latest_message(line)
        except IndexError, err:
            err_msg = "An error occured (%s). Please, make sure each entry " \
                      "is correctly formatted (each entry should begin with " \
                      "'%s'" % (err, changelog.BULLET)
            utils.show_msg(err_msg, gtk.MESSAGE_ERROR)
            # FIXME need a better error management
            raise
        return entry
    
    def _commit(self):
        """write back the model's content (the form is completed at this point)
        """
        self.update_entry()
        self._chlg.save()
        log(LOG_INFO, 'ChangeLog saved')

    def _get_entry(self):
        """return the changelog's entry associated with the model"""
        try:
            entry = self._entry
        except AttributeError:
            version = changelog.get_pkg_version(self.thedir.abspath)
            chlg = self._get_changelog()
            try:
                entry = chlg.get_entry(version, create=True)
            except (changelog.EntryNotFound, changelog.NoEntry):
                entry = chlg.get_entry(create=True)
            self._entry = entry
        return entry
    
    def _get_changelog(self):
        """return the changelog object associated with the model"""
        try:
            chlg = self._chlg
        except AttributeError:
            chlg = self._chlg = self.cl_class(self.file)
        return chlg

class DebianChangeLogModel(ChangeLogModel):
    """a changelog specific model"""
    cl_class = changelog.DebianChangeLog
    
    def __init__(self, thedir, close=False, filename='debian/changelog'):
        schema = (
            ('package', 'string', 'package', (), False, self.default_package), 
            ('version', 'string', 'version', (), False, self.default_version), 
            ('distrib', 'string', 'distrib', (), False, 'unstable'), 
            ('urgent', 'string', 'urgent', (), False, 'low'), 
            ('author', 'string', 'author', (), False, self.default_author), 
            ('email', 'string', 'author email', (), False, self.default_email),
            ('date', 'string', 'date', (), False, self.default_date), 
            ('entries', 'text', 'entries', (), True, self.default_entries), 
            )
        ChangeLogModel.__init__(self, thedir, close, schema, filename)

    def default_package(self):
        return 'FIXME'
    
    def default_author(self):
        return 'Logilab'

    def default_email(self):
        return 'devel@logilab.fr'
    
    def default_date(self):
        """get the current change log version"""
        entry = self._get_entry()
        if not entry.date:
            entry.date = self._get_changelog().formatted_date()
        self._entry = entry
        return entry.date

    def update_entry(self):
        entry = ChangeLogModel.update_entry(self)
        entry.author = '%s <%s>' % (self.get_value('author'),
                                    self.get_value('email'))
        entry.package = self.get_value('package')
        entry.distrib = self.get_value('distrib')
        entry.urgency = self.get_value('urgent')
        
class ChangeLogWindow(ModalFormWindow):
    """a window to edit ChangeLog latest entries"""
    
    def __init__(self, thedir, close=False,
                 filename='ChangeLog', model=ChangeLogModel):
        super(ChangeLogWindow, self).__init__(filename, model(thedir, close, filename=filename))
        wdg = wgenerator.generate(self.model.schema, self.model, self.ctrl)
        self.win.vbox.pack_end(wdg)
        self.win.show_all()
        # FIXME: why do I have to call notify to set form values according
        #        to the model ??
        self.model.notify_all()


# Check package, test coverage windows ########################################

class CheckPackageWindow(DevtoolsLogWindow):
    name = 'checkpackage'
        
    def __init__(self):
        DevtoolsLogWindow.__init__(self, 'check_package.ini')
        
    def check(self, thefile):
        """check the given wrapped package using checkpackage in a safe
        environment
        """
        self.clear()
        self.show()
        self.set_base(thefile)
        self.set_title('Check package: %s' % thefile.abspath)
        call_and_restore(self._check, thefile)
        
    def _check(self, thefile):
        """check the given wrapped package using checkpackage"""
        package_dir = thefile.abspath
        for func_name in check_package.__all__:
            check_func = getattr(check_package, func_name)
            try:
                check_func(self, package_dir, '__pkginfo__')
            except TypeError:
                check_func(self, package_dir)


class TestCoverageWindow(ConfigurableListWindow):
    name = 'test-coverage'
        
    def __init__(self):
        ConfigurableListWindow.__init__(self, 'check_package.ini',
                                        (gobject.TYPE_STRING, # path
                                         gobject.TYPE_INT, # statements num
                                         gobject.TYPE_INT, # executed statements
                                         gobject.TYPE_INT, # % executed statements
                                         gobject.TYPE_STRING, # % not executed
                                         ),
                                        ('Path', 'Statements', 'Executed', 'Covered', 'Missing'),
                                        )
        self.win.set_default_size(500, 400)
        self.init_columns(self.cfg['columns'], (0, 0, 0, 0, 0))
        self._cover_tool = coverage.Coverage()
        
    def extract_coverage(self, package_dir, coverage_file):
        """check the given wrapped package using checkpackage in a safe
        environment
        """
        self.clear()
        self.set_title('Test coverage: %s' % package_dir)
        self.show()
        self._cover_tool.restore(coverage_file)
        stats = self._cover_tool.report_stat(package_dir, ignore_errors=0)
        for name in stats.keys():
            if name == coverage.TOTAL_ENTRY:
                continue
            self.append_line( (name,) + stats[name] )


# debianize callbacks #########################################################

def debianize_replace(tmp_file, dest_file):
    """interactivly replace the old file with the new file according to
    user decision
    """
    if exists(dest_file):
        p_output = os.popen('diff -u %s %s' % (dest_file, tmp_file), 'r')
        diffs = p_output.read()
        if diffs:
            if utils.confirm('Differences:\n\n%s\n\nApply update ?' % diffs):
##                try:
                ensure_fs_mode(dest_file, S_IWRITE)
                shutil.copyfile(tmp_file, dest_file)
##                except IOError:
##                    os.system('cvs edit %s'%dest_file)
##                    shutil.copyfile(tmp_file, dest_file)
                log(LOG_INFO, '%s replaced', dest_file)
            else:
                log(LOG_INFO, 'keep current version of %s', dest_file)
        else:
            log(LOG_INFO, '%s created', dest_file)
    else:
        shutil.copyfile(tmp_file, dest_file)
        log(LOG_INFO, '%s created', dest_file)
    os.remove(tmp_file)


def debianize_empty(dest_file):
    """helper function for files to remove
    """
    if exists(dest_file):
        if utils.confirm('updated %s is empty. Remove it ?' % dest_file):
            log(LOG_INFO, '%s removed', dest_file)
            os.remove(dest_file)
        else:
            log(LOG_INFO, 'keep current version of %s', dest_file)


# The release manager #########################################################

class ReleaseManager:
    """The release manager is responsible to do release related commands.
    As those commands are interdependant, each action is build from a list
    of basic actions which should be executed, and different commands may
    share the same actions.
    
    FIXME missing actions:
    
    # read/write debian changelog
    
    # test package
    #   dput
    #   ssh / chroot
    #   apt-get
    #   runtest
    
    # validate package
    #   commit
    #   tag package
    #   debsign
    #   dput
    """
    name = 'release manager'
    options = (
        ('debian-build-command',
         {'type': 'string',
          'default': buildeb.BUILD_COMMAND}),
        ('setup-template',
         {'type': 'string',
          'default': join(TEMPLATE_DIR, 'setup.py')}),
        ('local-repository',
         {'type' : 'string',
          'default' : join(os.environ.get('HOME', ''), 'public_html', 'debian',
                           'mini-dinstall', 'incoming'),}),
        ('chroot-dir',
         {'type' : "string",
          'default': join('/sandbox', 'sarge-%s' % os.getlogin())},),
        )
    def __init__(self):
        self._commands = {
            'commit': ( self.action_check_uptodate, ),
            'setup': ( self.action_setup, ),
            'pkginfo': ( self.action_pkginfo, ),
            'debianize': ( self.action_debianize, ),
            'changelog': ( self.action_changelog, ),
            'source': ( self.action_source_distrib, ),
            
            'build': ( self.action_check_uptodate,
                       self.action_check_pkginfo, self.action_setup,
                       self.action_check_debian, self.action_build_distrib ),
            
            'upstream': ( self.action_check_uptodate,
                          self.action_pkginfo, self.action_setup,
                          self.action_readme, self.action_changelog,
                          self.action_make_doc,
                          self.action_debianize, self.action_debian_changelog,
                          self.action_build_distrib, self.action_tag_package),
            
            'debian': ( self.action_check_uptodate,
                        self.action_check_pkginfo, self.action_setup,
                        self.action_check_debian, self.action_debian_changelog,
                        self.action_build_distrib ),
            
            'validate': ( self.action_check_uptodate,
                          self.action_check_pkginfo, self.action_setup,
                          self.action_validate_distrib, self.action_tag_package),
            }
        self.cfg = PluggableConfig(self.name, 'release-manager.ini', self.options)
        self._shell = None

    def _get_shell(self):
        """returns a shell to execute command when needed"""
        if self._shell is None:
            self._shell = SystemCommandWindow()
        return self._shell
        # return SystemCommandWindow()
    shell = property(_get_shell, doc = "The release manager's shell")

    def subconfiguration(self):
        return []
    
    def do(self, thedir, command):
        """execute a command (the command determines the actions chain"""
        self._current_cmd = command
        actions_chain = self._commands[command]
        for action in actions_chain:
            log(LOG_DEBUG, 'going to action %s', action.__name__)
            try:
                if not action(thedir):
                    log(LOG_DEBUG, 'abort actions chain')
                    break
            except Exception, ex:
                import traceback
                traceback.print_exc()
                utils.show_msg('Error: %s' % ex, dlg_type=gtk.MESSAGE_ERROR)
                break
        else:
            if len(actions_chain) > 1:
                utils.show_msg('%s completed' % command)
        self._current_cmd = None
        
    def action_check_uptodate(self, thedir):
        """check for not up-to-date files"""
        not_up_to_date = thedir.not_up_to_date()
        if not_up_to_date:
            if utils.confirm('Package is not up to date: \n%s\n Do you want to '
                             'commit these files ?' % '\n'.join(["%s: %s"%r for r in not_up_to_date])):
                cmd = thedir.commit(ask_for_message('Enter your commit message'))
                if self.shell.execute(cmd) != 0:
                    self.shell.hide()
                    return False 
            elif utils.confirm('Abort release ?'):
                self.shell.hide()
                return False
        return True

    def action_check_pkginfo(self, thedir):
        """return true if the package has all necessary files to create/build
        a distribution
        """
        pkginfo_file = join(thedir.abspath, '__pkginfo__.py')
        if not exists(pkginfo_file):
            return self.action_pkginfo(thedir)
        return True
    
    def action_check_debian(self, thedir):
        """return true if the package has all necessary files to create/build
        a debian distribution
        """
        if not exists(join(thedir.abspath, 'debian')):
            return self.action_debianize(thedir)
        return True
    
    def action_pkginfo(self, thedir):
        """open a window to edit __pkginfo__ fields"""
        window = PkgInfoWindow(join(thedir.abspath, '__pkginfo__.py'))
        result = window.run()
        window.win.destroy()
        return result == gtk.RESPONSE_ACCEPT
    
    def action_setup(self, thedir):
        """create a default setup.py"""
        setup_file = join(thedir.abspath, 'setup.py')
        if not exists(setup_file):
            shutil.copy(self.cfg['setup-template'], setup_file)
            thedir.force_update()
            if utils.confirm('setup.py file has been created. Do you want to edit it ?'):
                # FIXME: wait and return True
                self.appl.editor.open(setup_file)
                return False
        # FIXME: check for differences and propose to update
        return True

    def action_readme(self, thedir):
        """create a default README"""
        readme_file = join(thedir.abspath, 'README')
        if not exists(readme_file):
            pi = call_and_restore(pkginfo.PackageInfo, directory=thedir.abspath)
            stream = open(readme_file, 'w')
            stream.write(README_TEMPLATE % (pi.name, "="*len(pi.name),
                                            pi.long_desc, pi.license_text))
            stream.close()
            thedir.force_update()
            if not utils.confirm('default README file has been created.\n'
                                 'Do you want to commit it ?\n'
                                 '(You can edit it manually, and then click on OK\n'):
                readme = thedir.create_child('README')
                add_cmd = readme.add()
                commit_cmd = thedir.commit('Added REAME')
                self.shell.show()
                if self.shell.execute(add_cmd) != 0:
                    self.shell.hide()
                    return False
                if self.shell.execute(commit_cmd) != 0:
                    self.shell.hide()
                    return False
                # FIXME: wait and return True
                # self.appl.editor.open(readme_file)
                self.shell.hide()
                return False
        return True
    
    def action_make_doc(self, thedir):
        """create a source distribution using a setup.py file"""
        if exists(join(thedir.abspath, 'doc', 'makefile')):
            self.execute_in_console('cd %s/doc' % thedir.abspath)
            self.execute_in_console('make')
            thedir.force_update()
        return True

    def action_changelog(self, thedir):
        """edit an upstream ChangeLog"""
        window = ChangeLogWindow(thedir, self._current_cmd == 'upstream')
        result = window.run()
        window.win.destroy()
        del window
        return result == gtk.RESPONSE_ACCEPT
    
    def action_debian_changelog(self, thedir):
        """edit a debian changelog"""
        window = ChangeLogWindow(thedir, self._current_cmd == 'upstream',
                                 filename='debian/changelog',
                                 model=DebianChangeLogModel)
        result = window.run()
        window.win.destroy()
        del window
        return result == gtk.RESPONSE_ACCEPT
    
    def action_debianize(self, thedir):
        """debianize a package"""
        reporter = DevtoolsLogWindow('debianize_display.ini','debianize log')
        reporter.set_title('Debianize : %s' % thedir.abspath)
        reporter.editor = self.editor
        reporter.set_base(thedir)
        reporter.show()
        debianize.debianize(reporter=reporter, directory=thedir.abspath,
                            replace_func=debianize_replace,
                            empty_func=debianize_empty)
        thedir.force_update()
        if self._current_cmd != 'debianize' \
               and utils.confirm('debianize succeed, continue ?'):
            reporter.win.destroy()
            return True
        return False
        
    
    def action_source_distrib(self, thedir):
        """create a source distribution using a setup.py file"""
        self.execute_in_console('cd %s' % thedir.abspath)
        self.execute_in_console('python setup.py sdist')
        thedir.force_update()
        return True
    
    def action_build_distrib(self, thedir):
        """create a debian distribution using build_debian"""
        self.shell.show()
        sys.stdout = self.shell
        sys.stderr = self.shell
        try:
            buildeb.build_debian(dest_dir=join(thedir.abspath, 'dist'),
                                 pkg_dir=thedir.abspath,
                                 command=self.cfg['debian-build-command'],
                                 system=self.shell.execute)
        finally:
            sys.stdout = sys.__stdout__
            sys.stderr = sys.__stderr__
        return True
    
    def action_validate_distrib(self, thedir):
        return True

    def action_tag_package(self, thedir):
        """adds release tags on package"""
        pi = call_and_restore(pkginfo.PackageInfo, directory=thedir.abspath)
        release_tag = pi.release_tag()
        cmds = []
        if utils.confirm('Do you want to tag package with %r' % release_tag):
            cmds.append(thedir.tag(release_tag))
        package_dir = join(thedir.abspath, 'debian')
        if exists(package_dir):
            debian_tag = pi.debian_release_tag()
            if utils.confirm('Do you want to tag package with %r' % debian_tag):
                cmds.append(thedir.tag(debian_tag))
        if cmds:
            for cmd in cmds:
                if self.shell.execute(cmd) != 0:
                    return False 
        return True

    def action_debsign(self, thedir):
        """signs the debian packages"""
        return True
    

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

class DevtoolsDirectoryPlugin(AbstractPlugIn):
    """Plugin for Logilab's developpement tools support"""
    
    name = 'devtools'
    mimetypes = ()

    def __init__(self):
        AbstractPlugIn.__init__(self)
        self.check_pkg_window = CheckPackageWindow()
        self.test_cover_window = TestCoverageWindow()
        self._rm = ReleaseManager()
        
    def set_application(self, appl):
        """set the application (on registration)"""
        AbstractPlugIn.set_application(self, appl)
        # FIXME: monkey patch
        self.check_pkg_window.editor = self.appl.editor
        self._rm.editor = self.appl.editor
        self._rm.execute_in_console = self.execute_in_console
        
    def subconfiguration(self):
        return [self._rm, self.check_pkg_window, self.test_cover_window]
    
    def support_directory(self, thedir):
        """return true if the directory contains at least one python module
        """
        return bool(glob.glob(join(thedir.abspath, '*.py')))
    
    def get_actions(self, thefile):
        """return actions provided by this plugin for the given directory
        """
        pkg_actions, dist_actions = [], []
        actions = [('packaging', pkg_actions), ('distribution', dist_actions)]
        filepath = thefile.abspath
        if exists(join(filepath, '__pkginfo__.py')) or is_root_package(filepath):
            actions.insert(0, ('check', self.cb_check_package) )
            pkg_actions.append( ('package information',
                                 self.cb_do_release_action, 'pkginfo') )
            if not exists(join(filepath, 'setup.py')):
                pkg_actions.append( ('create default setup.py',
                                     self.cb_do_release_action, 'setup') )
            pkg_actions.append( ('debianize',
                                 self.cb_do_release_action, 'debianize') )
            dist_actions.append( ('new upstream distribution',
                                  self.cb_do_release_action, 'upstream') )
            dist_actions.append( ('new debian distribution',
                                  self.cb_do_release_action, 'debian') )
            dist_actions.append( ('build source distribution',
                                  self.cb_do_release_action, 'source') )
            dist_actions.append( ('build distribution',
                                  self.cb_do_release_action, 'build') )
            dist_actions.append( ('validate distribution',
                                  self.cb_do_release_action, 'validate') )
            thefile.root_package = True
        else:
            thefile.root_package = False
        if exists(join(filepath, 'ChangeLog')):
            actions.append( ('add change log entry',
                             self.cb_do_release_action, 'changelog') )
        if exists(join(filepath, 'runtests.py')):
            actions.append( ('run', self.cb_run_tests) )
            actions.append( ('covered run', self.cb_covered_tests) )
        return actions

    def cb_run_tests(self, menuitem, thedir):
        self.execute_in_console('cd %s' % thedir.abspath)
        self.execute_in_console('python runtests.py')
        
    def cb_covered_tests(self, menuitem, thedir):
        # FIXME: currently execute_in_console is returning without waiting the
        # end of the command
        self.execute_in_console('cd %s' % thedir.abspath)
        self.execute_in_console('pycoverage -xe runtests.py')
        self.test_cover_window.extract_coverage(split(thedir.abspath)[0],
                                                join(thedir.abspath, '.coverage'))
        
    def cb_check_package(self, menuitem, thedir):
        """run checkpackage on a python package"""
        self.check_pkg_window.check(thedir)
    
    def cb_add_change_log_entry(self, menuitem, thedir):
        """add an entry to the ChangeLog file"""
        raise NotImplementedError()
    
    def cb_do_release_action(self, menuitem, thedir, action):
        """create a debian distribution using build_debian"""
        self._rm.do(thedir, action)


class DevtoolsFilePlugin(AbstractPlugIn):
    """Plugin for Logilab's developpement tools file support"""
    
    name = 'devtools-file'
    mimetypes = 'text/x-python',

    def get_actions(self, thefile):
        """return actions provided by this plugin for the given directory
        """
        return []

    
def register(registry):
    """register plugins from this module to the plugins registry"""
    registry.register(DevtoolsDirectoryPlugin())
    #registry.register(DevtoolsFilePlugin())