summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSylvain <syt@logilab.fr>2008-03-20 13:44:07 +0100
committerSylvain <syt@logilab.fr>2008-03-20 13:44:07 +0100
commit949b73d0d0790dc1d0363a441631a04b4a08942f (patch)
tree13d7d0eea092a2e591f6c678252f8a6e461fbf9a
parent82cd21d93cc316b672be1e5b30d659c0f697162e (diff)
downloadlogilab-common-949b73d0d0790dc1d0363a441631a04b4a08942f.tar.gz
* clcommands: commands may be hidden (eg not displayed in help), generic
ListCommandsCommand useful to build bash completion helpers * changelog: module to parse ChangeLog file as this one, backported from logilab.devtools
-rw-r--r--ChangeLog4
-rw-r--r--changelog.py194
-rw-r--r--clcommands.py41
3 files changed, 233 insertions, 6 deletions
diff --git a/ChangeLog b/ChangeLog
index 0b6b901..c8b9e3a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -3,6 +3,10 @@ ChangeLog for logilab.common
--
* db: remember logged user on the connection
+ * clcommands: commands may be hidden (eg not displayed in help), generic
+ ListCommandsCommand useful to build bash completion helpers
+ * changelog: module to parse ChangeLog file as this one, backported from
+ logilab.devtools
2008-03-12 -- 0.29.1
* date: new nb_open_days function counting worked days between two date
diff --git a/changelog.py b/changelog.py
new file mode 100644
index 0000000..5518d3e
--- /dev/null
+++ b/changelog.py
@@ -0,0 +1,194 @@
+# 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.
+"""Manipulation of upstream change log files
+
+The upstream change log files format handled is simpler than the one
+often used such as those generated by the default Emacs changelog mode.
+
+Sample ChangeLog format:
+------------------------------------------------------------
+Change log for project Yoo
+==========================
+
+ --
+ * add a new functionnality
+
+2002-02-01 -- 0.1.1
+ * fix bug #435454
+ * fix bug #434356
+
+2002-01-01 -- 0.1
+ * initial release
+
+------------------------------------------------------------
+
+There is 3 entries in this change log, one for each released version and one
+for the next version (i.e. the current entry).
+Each entry contains a set of messages corresponding to changes done in this
+release.
+All the non empty lines before the first entry are considered as the change
+log title.
+
+:author: Logilab
+:copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE)
+:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org
+"""
+
+import sys
+from stat import S_IWRITE
+
+from logilab.common.fileutils import ensure_fs_mode
+
+BULLET = '*'
+INDENT = ' '
+
+class NoEntry(Exception):
+ """raised when we are unable to find an entry"""
+
+class EntryNotFound(Exception):
+ """raised when we are unable to find a given entry"""
+
+class Version(tuple):
+ """simple class to handle soft version number has a tuple while
+ correctly printing it as X.Y.Z
+ """
+ def __new__(klass, versionstr):
+ if isinstance(versionstr, basestring):
+ parsed = [int(i) for i in versionstr.split('.')]
+ else:
+ parsed = versionstr
+ return tuple.__new__(klass, parsed)
+
+ def __str__(self):
+ return '.'.join([str(i) for i in self])
+
+# upstream change log #########################################################
+
+class ChangeLogEntry(object):
+ """a change log entry, ie a set of messages associated to a version and
+ its release date
+ """
+ version_class = Version
+
+ def __init__(self, date=None, version=None, **kwargs):
+ self.__dict__.update(kwargs)
+ if version:
+ self.version = self.version_class(version)
+ else:
+ self.version = None
+ self.date = date
+ self.messages = []
+
+ def add_message(self, msg):
+ """add a new message"""
+ self.messages.append([msg])
+
+ def complete_latest_message(self, msg_suite):
+ """complete the latest added message
+ """
+ if not self.messages:
+ print >> sys.stderr, 'Ignoring %r (unexpected format)' % msg_suite
+ self.messages[-1].append(msg_suite)
+
+ def write(self, stream=sys.stdout):
+ """write the entry to file """
+ stream.write('%s -- %s\n' % (self.date or '', self.version or ''))
+ for msg in self.messages:
+ stream.write('%s%s %s\n' % (INDENT, BULLET, msg[0]))
+ stream.write(''.join(msg[1:]))
+
+
+class ChangeLog(object):
+ """object representation of a whole ChangeLog file"""
+
+ entry_class = ChangeLogEntry
+
+ def __init__(self, changelog_file, title=''):
+ self.file = changelog_file
+ self.title = title
+ self.additional_content = ''
+ self.entries = []
+ self.load()
+
+ def __repr__(self):
+ return '<ChangeLog %s at %s (%s entries)>' % (self.file, id(self),
+ len(self.entries))
+
+ def add_entry(self, entry):
+ """add a new entry to the change log"""
+ self.entries.append(entry)
+
+ def get_entry(self, version='', create=None):
+ """ return a given changelog entry
+ if version is omited, return the current entry
+ """
+ if not self.entries:
+ if version or not create:
+ raise NoEntry()
+ self.entries.append(self.entry_class())
+ if not version:
+ if self.entries[0].version and create is not None:
+ self.entries.insert(0, self.entry_class())
+ return self.entries[0]
+ version = self.version_class(version)
+ for entry in self.entries:
+ if entry.version == version:
+ return entry
+ raise EntryNotFound()
+
+ def add(self, msg, create=None):
+ """add a new message to the latest opened entry"""
+ entry = self.get_entry(create=create)
+ entry.add_message(msg)
+
+ def load(self):
+ """ read a logilab's ChangeLog from file """
+ try:
+ stream = open(self.file)
+ except IOError:
+ return
+ last = None
+ for line in stream.readlines():
+ sline = line.strip()
+ words = sline.split()
+ if len(words) == 1 and words[0] == '--':
+ last = self.entry_class()
+ self.add_entry(last)
+ elif len(words) == 3 and words[1] == '--':
+ last = self.entry_class(words[0], words[2])
+ self.add_entry(last)
+ elif last is None:
+ if not sline:
+ continue
+ self.title = '%s%s' % (self.title, line)
+ elif sline and sline[0] == BULLET:
+ last.add_message(sline[1:].strip())
+ elif last.messages:
+ last.complete_latest_message(line)
+ else:
+ self.additional_content += line
+ stream.close()
+
+ def format_title(self):
+ return '%s\n\n' % self.title.strip()
+
+ def save(self):
+ """write back change log"""
+ ensure_fs_mode(self.file, S_IWRITE)
+ self.write(open(self.file, 'w'))
+
+ def write(self, stream=sys.stdout):
+ """write changelog to stream"""
+ stream.write(self.format_title())
+ for entry in self.entries:
+ entry.write(stream)
diff --git a/clcommands.py b/clcommands.py
index 71427cf..d3ebbe6 100644
--- a/clcommands.py
+++ b/clcommands.py
@@ -1,6 +1,3 @@
-# Copyright (c) 2004-2007 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
@@ -17,6 +14,10 @@
one command
e.g called as "tool command [options] args..." where <options> and <args> are
command'specific
+
+:author: Logilab
+:copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE)
+:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org
"""
import sys
@@ -26,7 +27,7 @@ from logilab.common.configuration import Configuration
DEFAULT_COPYRIGHT = '''\
-Copyright (c) 2004-2007 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+Copyright (c) 2004-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
@@ -40,6 +41,8 @@ class Command(Configuration):
"""base class for command line commands"""
arguments = ''
name = ''
+ # hidden from help ?
+ hidden = False
# max/min args, None meaning unspecified
min_args = None
max_args = None
@@ -98,8 +101,9 @@ command. Available commands are :\n''')
padding = ' '*max_len
for command in commands:
cmd = _COMMANDS[command]
- title = cmd.__doc__.split('.')[0]
- print ' ', (command+padding)[:max_len], title
+ if not cmd.hidden:
+ title = cmd.__doc__.split('.')[0]
+ print ' ', (command+padding)[:max_len], title
print '\n', copyright
sys.exit(status)
@@ -133,3 +137,28 @@ def main_run(args, doc):
except BadCommandUsage, err:
print 'ERROR: ', err
main_usage(1, doc)
+
+
+class ListCommandsCommand(Command):
+ """list available commands, useful for bash completion."""
+ name = 'listcommands'
+ arguments = '[command]'
+ hidden = True
+
+ def run(self, args):
+ """run the command with its specific arguments"""
+ if args:
+ command = pop_arg(args)
+ cmd = _COMMANDS[command]
+ for optname, optdict in cmd.options:
+ print '--help'
+ print '--' + optname
+ else:
+ commands = _COMMANDS.keys()
+ commands.sort()
+ for command in commands:
+ cmd = _COMMANDS[command]
+ if not cmd.hidden:
+ print command
+
+register_commands([ListCommandsCommand])