Moved everything into artemis/ subdirectory to be able to import termcolor.py
authorDmitriy Morozov <dmitriy@mrzv.org>
Fri, 15 Apr 2011 23:10:12 -0700
changeset 60 c933fa2cd204
parent 59 18da6a9fa7b8
child 61 c384fa42f8a2
Moved everything into artemis/ subdirectory to be able to import termcolor.py
README
artemis.py
artemis/__init__.py
artemis/artemis.py
artemis/termcolor.py
termcolor.py
--- a/README	Fri Apr 15 22:57:55 2011 -0700
+++ b/README	Fri Apr 15 23:10:12 2011 -0700
@@ -27,7 +27,7 @@
 
 In the ``[extensions]`` section of your ``~/.hgrc`` add::
 
-    artemis = /path/to/artemis.py
+    artemis = /path/to/Artemis/artemis
 
 Optionally, provide a section ``[artemis]``, and specify an alternative path for
 the issues subdirectory (instead of the default ``.issues``)::
--- a/artemis.py	Fri Apr 15 22:57:55 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,487 +0,0 @@
-# Author: Dmitriy Morozov <hg@foxcub.org>, 2007 -- 2009
-
-"""A very simple and lightweight issue tracker for Mercurial."""
-
-from mercurial import hg, util, commands
-from mercurial.i18n import _
-import os, time, random, mailbox, glob, socket, ConfigParser
-import mimetypes
-from email import encoders
-from email.generator import Generator
-from email.mime.audio import MIMEAudio
-from email.mime.base import MIMEBase
-from email.mime.image import MIMEImage
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-
-from    termcolor       import colored
-
-
-state = { 'new':   ['new'],
-          'fixed': ['fixed', 'resolved'] }
-annotation = { 'resolved': 'resolution' }
-default_state = 'new'
-default_issues_dir = ".issues"
-filter_prefix = ".filter"
-date_format = '%a, %d %b %Y %H:%M:%S %1%2'
-maildir_dirs = ['new','cur','tmp']
-
-
-def ilist(ui, repo, **opts):
-    """List issues associated with the project"""
-
-    # Process options
-    show_all = opts['all']
-    properties = []
-    match_date, date_match = False, lambda x: True
-    if opts['date']:
-        match_date, date_match = True, util.matchdate(opts['date'])
-    order = 'new'
-    if opts['order']:
-        order = opts['order']
-
-    # Colors
-    colors = _read_colors(ui)
-
-    # Find issues
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    issues_path = os.path.join(repo.root, issues_dir)
-    if not os.path.exists(issues_path): return
-
-    issues = glob.glob(os.path.join(issues_path, '*'))
-
-    _create_all_missing_dirs(issues_path, issues)
-
-    # Process filter
-    if opts['filter']:
-        filters = glob.glob(os.path.join(issues_path, filter_prefix + '*'))
-        config = ConfigParser.SafeConfigParser()
-        config.read(filters)
-        if not config.has_section(opts['filter']):
-            ui.write('No filter %s defined\n' % opts['filter'])
-        else:
-            properties += config.items(opts['filter'])
-
-    cmd_properties = _get_properties(opts['property'])
-    list_properties = [p[0] for p in cmd_properties if len(p) == 1]
-    list_properties_dict = {}
-    properties += filter(lambda p: len(p) > 1, cmd_properties)
-
-    summaries = []
-    for issue in issues:
-        mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
-        root = _find_root_key(mbox)
-        if not root: continue
-        property_match = True
-        for property,value in properties:
-            if value:
-                property_match = property_match and (mbox[root][property] == value)
-            else:
-                property_match = property_match and (property not in mbox[root])
-
-        if not show_all and (not properties or not property_match) and (properties or mbox[root]['State'].upper() in [f.upper() for f in state['fixed']]): continue
-        if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
-
-        if not list_properties:
-            summaries.append((_summary_line(mbox, root, issue[len(issues_path)+1:], colors),     # +1 for trailing /
-                              _find_mbox_date(mbox, root, order)))
-        else:
-            for lp in list_properties:
-                if lp in mbox[root]:    list_properties_dict.setdefault(lp, set()).add(mbox[root][lp])
-
-    if not list_properties:
-        summaries.sort(lambda (s1,d1),(s2,d2): cmp(d2,d1))
-        for s,d in summaries:
-            ui.write(s)
-    else:
-        for lp in list_properties_dict.keys():
-            ui.write("%s:\n" % lp)
-            for value in sorted(list_properties_dict[lp]):
-                ui.write("  %s\n" % value)
-
-
-def iadd(ui, repo, id = None, comment = 0, **opts):
-    """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
-
-    comment = int(comment)
-
-    # First, make sure issues have a directory
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    issues_path = os.path.join(repo.root, issues_dir)
-    if not os.path.exists(issues_path): os.mkdir(issues_path)
-
-    if id:
-        issue_fn, issue_id = _find_issue(ui, repo, id)
-        if not issue_fn:
-            ui.warn('No such issue\n')
-            return
-        _create_missing_dirs(issues_path, issue_id)
-
-    user = ui.username()
-
-    default_issue_text  =         "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format))
-    if not id:
-        default_issue_text +=     "State: %s\n" % default_state
-    default_issue_text +=         "Subject: brief description\n\n"
-    default_issue_text +=         "Detailed description."
-
-    # Get properties, and figure out if we need an explicit comment
-    properties = _get_properties(opts['property'])
-    no_comment = id and properties and opts['no_property_comment']
-    message = opts['message']
-
-    # Create the text
-    if message:
-        if not id:
-            state_str = 'State: %s\n' % default_state
-        else:
-            state_str = ''
-        issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
-                (user, util.datestr(format=date_format), message, state_str)
-    elif not no_comment:
-        issue = ui.edit(default_issue_text, user)
-
-        if issue.strip() == '':
-            ui.warn('Empty issue, ignoring\n')
-            return
-        if issue.strip() == default_issue_text:
-            ui.warn('Unchanged issue text, ignoring\n')
-            return
-    else:
-        # Write down a comment about updated properties
-        properties_subject = ', '.join(['%s=%s' % (property, value) for (property, value) in properties])
-
-        issue =     "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
-                     (user, util.datestr(format = date_format), properties_subject)
-
-    # Create the message
-    msg = mailbox.MaildirMessage(issue)
-    if opts['attach']:
-        outer = _attach_files(msg, opts['attach'])
-    else:
-        outer = msg
-
-    # Pick random filename
-    if not id:
-        issue_fn = issues_path
-        while os.path.exists(issue_fn):
-            issue_id = _random_id()
-            issue_fn = os.path.join(issues_path, issue_id)
-    # else: issue_fn already set
-
-    # Add message to the mailbox
-    mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
-    keys = _order_keys_date(mbox)
-    mbox.lock()
-    if id and comment >= len(mbox):
-        ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
-
-    if not id:
-        outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
-    else:
-        root = keys[0]
-        outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
-        outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
-        outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
-    new_bug_path = issue_fn[(len(repo.root)+1):] + '/new/' + mbox.add(outer) # + 1 for the trailing /
-    commands.add(ui, repo, new_bug_path)
-
-    # Fix properties in the root message
-    if properties:
-        root = _find_root_key(mbox)
-        msg = mbox[root]
-        for property, value in properties:
-            if property in msg:
-                msg.replace_header(property, value)
-            else:
-                msg.add_header(property, value)
-        mbox[root] = msg
-
-    mbox.close()
-
-    if opts['commit']:
-        commands.commit(ui, repo, issue_fn)
-
-    # If adding issue, add the new mailbox to the repository
-    if not id:
-        ui.status('Added new issue %s\n' % issue_id)
-    else:
-        _show_mbox(ui, mbox, 0)
-
-def ishow(ui, repo, id, comment = 0, **opts):
-    """Shows issue ID, or possibly its comment COMMENT"""
-
-    comment = int(comment)
-    issue, id = _find_issue(ui, repo, id)
-    if not issue:
-        return ui.warn('No such issue\n')
-
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    _create_missing_dirs(os.path.join(repo.root, issues_dir), issue)
-
-    if opts.get('mutt'):
-        return util.system('mutt -R -f %s' % issue)
-
-    mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
-
-    if opts['all']:
-        ui.write('='*70 + '\n')
-        i = 0
-        keys = _order_keys_date(mbox)
-        for k in keys:
-            _write_message(ui, mbox[k], i, skip = opts['skip'])
-            ui.write('-'*70 + '\n')
-            i += 1
-        return
-
-    _show_mbox(ui, mbox, comment, skip = opts['skip'])
-
-    if opts['extract']:
-        attachment_numbers = map(int, opts['extract'])
-        keys = _order_keys_date(mbox)
-        msg = mbox[keys[comment]]
-        counter = 1
-        for part in msg.walk():
-            ctype = part.get_content_type()
-            maintype, subtype = ctype.split('/', 1)
-            if maintype == 'multipart' or ctype == 'text/plain': continue
-            if counter in attachment_numbers:
-                filename = part.get_filename()
-                if not filename:
-                    ext = mimetypes.guess_extension(part.get_content_type()) or ''
-                    filename = 'attachment-%03d%s' % (counter, ext)
-                fp = open(filename, 'wb')
-                fp.write(part.get_payload(decode = True))
-                fp.close()
-            counter += 1
-
-
-def _find_issue(ui, repo, id):
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    issues_path = os.path.join(repo.root, issues_dir)
-    if not os.path.exists(issues_path): return False
-
-    issues = glob.glob(os.path.join(issues_path, id + '*'))
-
-    if len(issues) == 0:
-        return False, 0
-    elif len(issues) > 1:
-        ui.status("Multiple choices:\n")
-        for i in issues: ui.status('  ', i[len(issues_path)+1:], '\n')
-        return False, 0
-
-    return issues[0], issues[0][len(issues_path)+1:]
-
-def _get_properties(property_list):
-    return [p.split('=') for p in property_list]
-
-def _write_message(ui, message, index = 0, skip = None):
-    if index: ui.write("Comment: %d\n" % index)
-    if ui.verbose:
-        _show_text(ui, message.as_string().strip(), skip)
-    else:
-        if 'From' in message: ui.write('From: %s\n' % message['From'])
-        if 'Date' in message: ui.write('Date: %s\n' % message['Date'])
-        if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject'])
-        if 'State' in message: ui.write('State: %s\n' % message['State'])
-        counter = 1
-        for part in message.walk():
-            ctype = part.get_content_type()
-            maintype, subtype = ctype.split('/', 1)
-            if maintype == 'multipart': continue
-            if ctype == 'text/plain':
-                ui.write('\n')
-                _show_text(ui, part.get_payload().strip(), skip)
-            else:
-                filename = part.get_filename()
-                ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (counter, ctype, _humanreadable(len(part.get_payload())), filename) + '\n')
-                counter += 1
-
-def _show_text(ui, text, skip = None):
-    for line in text.splitlines():
-        if not skip or not line.startswith(skip):
-            ui.write(line + '\n')
-    ui.write('\n')
-
-def _show_mbox(ui, mbox, comment, **opts):
-    # Output the issue (or comment)
-    if comment >= len(mbox):
-        comment = 0
-        ui.warn('Comment out of range, showing the issue itself\n')
-    keys = _order_keys_date(mbox)
-    root = keys[0]
-    msg = mbox[keys[comment]]
-    ui.write('='*70 + '\n')
-    if comment:
-        ui.write('Subject: %s\n' % mbox[root]['Subject'])
-        ui.write('State: %s\n' % mbox[root]['State'])
-        ui.write('-'*70 + '\n')
-    _write_message(ui, msg, comment, skip = ('skip' in opts) and opts['skip'])
-    ui.write('-'*70 + '\n')
-
-    # Read the mailbox into the messages and children dictionaries
-    messages = {}
-    children = {}
-    i = 0
-    for k in keys:
-        m = mbox[k]
-        messages[m['Message-Id']] = (i,m)
-        children.setdefault(m['In-Reply-To'], []).append(m['Message-Id'])
-        i += 1
-    children[None] = []                # Safeguard against infinte loop on empty Message-Id
-
-    # Iterate over children
-    id = msg['Message-Id']
-    id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or []
-    if not id_stack: return
-    ui.write('Comments:\n')
-    while id_stack:
-        id,offset = id_stack.pop()
-        id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or []
-        index, msg = messages[id]
-        ui.write('  '*offset + '%d: [%s] %s\n' % (index, util.shortuser(msg['From']), msg['Subject']))
-    ui.write('-'*70 + '\n')
-
-def _find_root_key(maildir):
-    for k,m in maildir.iteritems():
-        if 'in-reply-to' not in m:
-            return k
-
-def _order_keys_date(mbox):
-    keys = mbox.keys()
-    root = _find_root_key(mbox)
-    keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
-    return keys
-
-def _find_mbox_date(mbox, root, order):
-    if order == 'latest':
-        keys = _order_keys_date(mbox)
-        msg = mbox[keys[-1]]
-    else:   # new
-        msg = mbox[root]
-    return util.parsedate(msg['date'])
-
-def _random_id():
-    return "%x" % random.randint(2**63, 2**64-1)
-
-def _create_missing_dirs(issues_path, issue):
-    for d in maildir_dirs:
-        path = os.path.join(issues_path,issue,d)
-        if not os.path.exists(path): os.mkdir(path)
-
-def _create_all_missing_dirs(issues_path, issues):
-    for i in issues:
-        _create_missing_dirs(issues_path, i)
-
-def _humanreadable(size):
-    if size > 1024*1024:
-        return '%5.1fM' % (float(size) / (1024*1024))
-    elif size > 1024:
-        return '%5.1fK' % (float(size) / 1024)
-    else:
-        return '%dB' % size
-
-def _attach_files(msg, filenames):
-    outer = MIMEMultipart()
-    for k in msg.keys(): outer[k] = msg[k]
-    outer.attach(MIMEText(msg.get_payload()))
-
-    for filename in filenames:
-        ctype, encoding = mimetypes.guess_type(filename)
-        if ctype is None or encoding is not None:
-            # No guess could be made, or the file is encoded (compressed), so
-            # use a generic bag-of-bits type.
-            ctype = 'application/octet-stream'
-        maintype, subtype = ctype.split('/', 1)
-        if maintype == 'text':
-            fp = open(filename)
-            # Note: we should handle calculating the charset
-            attachment = MIMEText(fp.read(), _subtype=subtype)
-            fp.close()
-        elif maintype == 'image':
-            fp = open(filename, 'rb')
-            attachment = MIMEImage(fp.read(), _subtype=subtype)
-            fp.close()
-        elif maintype == 'audio':
-            fp = open(filename, 'rb')
-            attachment = MIMEAudio(fp.read(), _subtype=subtype)
-            fp.close()
-        else:
-            fp = open(filename, 'rb')
-            attachment = MIMEBase(maintype, subtype)
-            attachment.set_payload(fp.read())
-            fp.close()
-            # Encode the payload using Base64
-            encoders.encode_base64(attachment)
-        # Set the filename parameter
-        attachment.add_header('Content-Disposition', 'attachment', filename=filename)
-        outer.attach(attachment)
-    return outer
-
-def _status_msg(msg):
-    s = msg['State']
-    if s in annotation:
-        return '%s=%s' % (s, msg[annotation[s]])
-    else:
-        return s
-
-def _read_colors(ui):
-    colors = {}
-    # defaults
-    colors['new.color']             = 'red'
-    colors['new.on_color']          = 'on_grey'
-    colors['new.attrs']             = 'bold'
-    colors['resolved.color']        = 'white'
-    colors['resolved.on_color']     = ''
-    colors['resolved.attrs']        = ''
-    for v in colors:
-        colors[v] = ui.config('artemis', v, colors[v])
-        if v.endswith('attrs'): colors[v] = colors[v].split()
-    return colors
-
-def _color_summary(line, msg, colors):
-    if msg['State'] == 'new':
-        return colored(line, colors['new.color'],      attrs = colors['new.attrs'])
-    elif msg['State'] in state['fixed']:
-        return colored(line, colors['resolved.color'], attrs = colors['resolved.attrs'])
-    else:
-        return line
-
-def _summary_line(mbox, root, issue, colors):
-    line = "%s (%3d) [%s]: %s\n" % (issue,
-                                    len(mbox)-1,                # number of replies (-1 for self)
-                                    _status_msg(mbox[root]),
-                                    mbox[root]['Subject'])
-    return _color_summary(line, mbox[root], colors)
-
-cmdtable = {
-    'ilist':    (ilist,
-                 [('a', 'all', False,
-                   'list all issues (by default only those with state new)'),
-                  ('p', 'property', [],
-                   'list issues with specific field values (e.g., -p state=fixed); lists all possible values of a property if no = sign'),
-                  ('o', 'order', 'new', 'order of the issues; choices: "new" (date submitted), "latest" (date of the last message)'),
-                  ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
-                  ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (default_issues_dir, filter_prefix))],
-                 _('hg ilist [OPTIONS]')),
-    'iadd':       (iadd,
-                 [('a', 'attach', [],
-                   'attach file(s) (e.g., -a filename1 -a filename2)'),
-                  ('p', 'property', [],
-                   'update properties (e.g., -p state=fixed)'),
-                  ('n', 'no-property-comment', None,
-                   'do not add a comment about changed properties'),
-                  ('m', 'message', '',
-                   'use <text> as an issue subject'),
-                  ('c', 'commit', False,
-                   'perform a commit after the addition')],
-                 _('hg iadd [OPTIONS] [ID] [COMMENT]')),
-    'ishow':      (ishow,
-                 [('a', 'all', None, 'list all comments'),
-                  ('s', 'skip', '>', 'skip lines starting with a substring'),
-                  ('x', 'extract', [], 'extract attachments (provide attachment number as argument)'),
-                  ('', 'mutt', False, 'use mutt to show issue')],
-                 _('hg ishow [OPTIONS] ID [COMMENT]')),
-}
-
-# vim: expandtab
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/__init__.py	Fri Apr 15 23:10:12 2011 -0700
@@ -0,0 +1,1 @@
+from artemis import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/artemis.py	Fri Apr 15 23:10:12 2011 -0700
@@ -0,0 +1,487 @@
+# Author: Dmitriy Morozov <hg@foxcub.org>, 2007 -- 2009
+
+"""A very simple and lightweight issue tracker for Mercurial."""
+
+from mercurial import hg, util, commands
+from mercurial.i18n import _
+import os, time, random, mailbox, glob, socket, ConfigParser
+import mimetypes
+from email import encoders
+from email.generator import Generator
+from email.mime.audio import MIMEAudio
+from email.mime.base import MIMEBase
+from email.mime.image import MIMEImage
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+from    termcolor       import colored
+
+
+state = { 'new':   ['new'],
+          'fixed': ['fixed', 'resolved'] }
+annotation = { 'resolved': 'resolution' }
+default_state = 'new'
+default_issues_dir = ".issues"
+filter_prefix = ".filter"
+date_format = '%a, %d %b %Y %H:%M:%S %1%2'
+maildir_dirs = ['new','cur','tmp']
+
+
+def ilist(ui, repo, **opts):
+    """List issues associated with the project"""
+
+    # Process options
+    show_all = opts['all']
+    properties = []
+    match_date, date_match = False, lambda x: True
+    if opts['date']:
+        match_date, date_match = True, util.matchdate(opts['date'])
+    order = 'new'
+    if opts['order']:
+        order = opts['order']
+
+    # Colors
+    colors = _read_colors(ui)
+
+    # Find issues
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    issues_path = os.path.join(repo.root, issues_dir)
+    if not os.path.exists(issues_path): return
+
+    issues = glob.glob(os.path.join(issues_path, '*'))
+
+    _create_all_missing_dirs(issues_path, issues)
+
+    # Process filter
+    if opts['filter']:
+        filters = glob.glob(os.path.join(issues_path, filter_prefix + '*'))
+        config = ConfigParser.SafeConfigParser()
+        config.read(filters)
+        if not config.has_section(opts['filter']):
+            ui.write('No filter %s defined\n' % opts['filter'])
+        else:
+            properties += config.items(opts['filter'])
+
+    cmd_properties = _get_properties(opts['property'])
+    list_properties = [p[0] for p in cmd_properties if len(p) == 1]
+    list_properties_dict = {}
+    properties += filter(lambda p: len(p) > 1, cmd_properties)
+
+    summaries = []
+    for issue in issues:
+        mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
+        root = _find_root_key(mbox)
+        if not root: continue
+        property_match = True
+        for property,value in properties:
+            if value:
+                property_match = property_match and (mbox[root][property] == value)
+            else:
+                property_match = property_match and (property not in mbox[root])
+
+        if not show_all and (not properties or not property_match) and (properties or mbox[root]['State'].upper() in [f.upper() for f in state['fixed']]): continue
+        if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
+
+        if not list_properties:
+            summaries.append((_summary_line(mbox, root, issue[len(issues_path)+1:], colors),     # +1 for trailing /
+                              _find_mbox_date(mbox, root, order)))
+        else:
+            for lp in list_properties:
+                if lp in mbox[root]:    list_properties_dict.setdefault(lp, set()).add(mbox[root][lp])
+
+    if not list_properties:
+        summaries.sort(lambda (s1,d1),(s2,d2): cmp(d2,d1))
+        for s,d in summaries:
+            ui.write(s)
+    else:
+        for lp in list_properties_dict.keys():
+            ui.write("%s:\n" % lp)
+            for value in sorted(list_properties_dict[lp]):
+                ui.write("  %s\n" % value)
+
+
+def iadd(ui, repo, id = None, comment = 0, **opts):
+    """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
+
+    comment = int(comment)
+
+    # First, make sure issues have a directory
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    issues_path = os.path.join(repo.root, issues_dir)
+    if not os.path.exists(issues_path): os.mkdir(issues_path)
+
+    if id:
+        issue_fn, issue_id = _find_issue(ui, repo, id)
+        if not issue_fn:
+            ui.warn('No such issue\n')
+            return
+        _create_missing_dirs(issues_path, issue_id)
+
+    user = ui.username()
+
+    default_issue_text  =         "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format))
+    if not id:
+        default_issue_text +=     "State: %s\n" % default_state
+    default_issue_text +=         "Subject: brief description\n\n"
+    default_issue_text +=         "Detailed description."
+
+    # Get properties, and figure out if we need an explicit comment
+    properties = _get_properties(opts['property'])
+    no_comment = id and properties and opts['no_property_comment']
+    message = opts['message']
+
+    # Create the text
+    if message:
+        if not id:
+            state_str = 'State: %s\n' % default_state
+        else:
+            state_str = ''
+        issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
+                (user, util.datestr(format=date_format), message, state_str)
+    elif not no_comment:
+        issue = ui.edit(default_issue_text, user)
+
+        if issue.strip() == '':
+            ui.warn('Empty issue, ignoring\n')
+            return
+        if issue.strip() == default_issue_text:
+            ui.warn('Unchanged issue text, ignoring\n')
+            return
+    else:
+        # Write down a comment about updated properties
+        properties_subject = ', '.join(['%s=%s' % (property, value) for (property, value) in properties])
+
+        issue =     "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
+                     (user, util.datestr(format = date_format), properties_subject)
+
+    # Create the message
+    msg = mailbox.MaildirMessage(issue)
+    if opts['attach']:
+        outer = _attach_files(msg, opts['attach'])
+    else:
+        outer = msg
+
+    # Pick random filename
+    if not id:
+        issue_fn = issues_path
+        while os.path.exists(issue_fn):
+            issue_id = _random_id()
+            issue_fn = os.path.join(issues_path, issue_id)
+    # else: issue_fn already set
+
+    # Add message to the mailbox
+    mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
+    keys = _order_keys_date(mbox)
+    mbox.lock()
+    if id and comment >= len(mbox):
+        ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
+
+    if not id:
+        outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
+    else:
+        root = keys[0]
+        outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
+        outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
+        outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
+    new_bug_path = issue_fn[(len(repo.root)+1):] + '/new/' + mbox.add(outer) # + 1 for the trailing /
+    commands.add(ui, repo, new_bug_path)
+
+    # Fix properties in the root message
+    if properties:
+        root = _find_root_key(mbox)
+        msg = mbox[root]
+        for property, value in properties:
+            if property in msg:
+                msg.replace_header(property, value)
+            else:
+                msg.add_header(property, value)
+        mbox[root] = msg
+
+    mbox.close()
+
+    if opts['commit']:
+        commands.commit(ui, repo, issue_fn)
+
+    # If adding issue, add the new mailbox to the repository
+    if not id:
+        ui.status('Added new issue %s\n' % issue_id)
+    else:
+        _show_mbox(ui, mbox, 0)
+
+def ishow(ui, repo, id, comment = 0, **opts):
+    """Shows issue ID, or possibly its comment COMMENT"""
+
+    comment = int(comment)
+    issue, id = _find_issue(ui, repo, id)
+    if not issue:
+        return ui.warn('No such issue\n')
+
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    _create_missing_dirs(os.path.join(repo.root, issues_dir), issue)
+
+    if opts.get('mutt'):
+        return util.system('mutt -R -f %s' % issue)
+
+    mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
+
+    if opts['all']:
+        ui.write('='*70 + '\n')
+        i = 0
+        keys = _order_keys_date(mbox)
+        for k in keys:
+            _write_message(ui, mbox[k], i, skip = opts['skip'])
+            ui.write('-'*70 + '\n')
+            i += 1
+        return
+
+    _show_mbox(ui, mbox, comment, skip = opts['skip'])
+
+    if opts['extract']:
+        attachment_numbers = map(int, opts['extract'])
+        keys = _order_keys_date(mbox)
+        msg = mbox[keys[comment]]
+        counter = 1
+        for part in msg.walk():
+            ctype = part.get_content_type()
+            maintype, subtype = ctype.split('/', 1)
+            if maintype == 'multipart' or ctype == 'text/plain': continue
+            if counter in attachment_numbers:
+                filename = part.get_filename()
+                if not filename:
+                    ext = mimetypes.guess_extension(part.get_content_type()) or ''
+                    filename = 'attachment-%03d%s' % (counter, ext)
+                fp = open(filename, 'wb')
+                fp.write(part.get_payload(decode = True))
+                fp.close()
+            counter += 1
+
+
+def _find_issue(ui, repo, id):
+    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
+    issues_path = os.path.join(repo.root, issues_dir)
+    if not os.path.exists(issues_path): return False
+
+    issues = glob.glob(os.path.join(issues_path, id + '*'))
+
+    if len(issues) == 0:
+        return False, 0
+    elif len(issues) > 1:
+        ui.status("Multiple choices:\n")
+        for i in issues: ui.status('  ', i[len(issues_path)+1:], '\n')
+        return False, 0
+
+    return issues[0], issues[0][len(issues_path)+1:]
+
+def _get_properties(property_list):
+    return [p.split('=') for p in property_list]
+
+def _write_message(ui, message, index = 0, skip = None):
+    if index: ui.write("Comment: %d\n" % index)
+    if ui.verbose:
+        _show_text(ui, message.as_string().strip(), skip)
+    else:
+        if 'From' in message: ui.write('From: %s\n' % message['From'])
+        if 'Date' in message: ui.write('Date: %s\n' % message['Date'])
+        if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject'])
+        if 'State' in message: ui.write('State: %s\n' % message['State'])
+        counter = 1
+        for part in message.walk():
+            ctype = part.get_content_type()
+            maintype, subtype = ctype.split('/', 1)
+            if maintype == 'multipart': continue
+            if ctype == 'text/plain':
+                ui.write('\n')
+                _show_text(ui, part.get_payload().strip(), skip)
+            else:
+                filename = part.get_filename()
+                ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (counter, ctype, _humanreadable(len(part.get_payload())), filename) + '\n')
+                counter += 1
+
+def _show_text(ui, text, skip = None):
+    for line in text.splitlines():
+        if not skip or not line.startswith(skip):
+            ui.write(line + '\n')
+    ui.write('\n')
+
+def _show_mbox(ui, mbox, comment, **opts):
+    # Output the issue (or comment)
+    if comment >= len(mbox):
+        comment = 0
+        ui.warn('Comment out of range, showing the issue itself\n')
+    keys = _order_keys_date(mbox)
+    root = keys[0]
+    msg = mbox[keys[comment]]
+    ui.write('='*70 + '\n')
+    if comment:
+        ui.write('Subject: %s\n' % mbox[root]['Subject'])
+        ui.write('State: %s\n' % mbox[root]['State'])
+        ui.write('-'*70 + '\n')
+    _write_message(ui, msg, comment, skip = ('skip' in opts) and opts['skip'])
+    ui.write('-'*70 + '\n')
+
+    # Read the mailbox into the messages and children dictionaries
+    messages = {}
+    children = {}
+    i = 0
+    for k in keys:
+        m = mbox[k]
+        messages[m['Message-Id']] = (i,m)
+        children.setdefault(m['In-Reply-To'], []).append(m['Message-Id'])
+        i += 1
+    children[None] = []                # Safeguard against infinte loop on empty Message-Id
+
+    # Iterate over children
+    id = msg['Message-Id']
+    id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or []
+    if not id_stack: return
+    ui.write('Comments:\n')
+    while id_stack:
+        id,offset = id_stack.pop()
+        id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or []
+        index, msg = messages[id]
+        ui.write('  '*offset + '%d: [%s] %s\n' % (index, util.shortuser(msg['From']), msg['Subject']))
+    ui.write('-'*70 + '\n')
+
+def _find_root_key(maildir):
+    for k,m in maildir.iteritems():
+        if 'in-reply-to' not in m:
+            return k
+
+def _order_keys_date(mbox):
+    keys = mbox.keys()
+    root = _find_root_key(mbox)
+    keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
+    return keys
+
+def _find_mbox_date(mbox, root, order):
+    if order == 'latest':
+        keys = _order_keys_date(mbox)
+        msg = mbox[keys[-1]]
+    else:   # new
+        msg = mbox[root]
+    return util.parsedate(msg['date'])
+
+def _random_id():
+    return "%x" % random.randint(2**63, 2**64-1)
+
+def _create_missing_dirs(issues_path, issue):
+    for d in maildir_dirs:
+        path = os.path.join(issues_path,issue,d)
+        if not os.path.exists(path): os.mkdir(path)
+
+def _create_all_missing_dirs(issues_path, issues):
+    for i in issues:
+        _create_missing_dirs(issues_path, i)
+
+def _humanreadable(size):
+    if size > 1024*1024:
+        return '%5.1fM' % (float(size) / (1024*1024))
+    elif size > 1024:
+        return '%5.1fK' % (float(size) / 1024)
+    else:
+        return '%dB' % size
+
+def _attach_files(msg, filenames):
+    outer = MIMEMultipart()
+    for k in msg.keys(): outer[k] = msg[k]
+    outer.attach(MIMEText(msg.get_payload()))
+
+    for filename in filenames:
+        ctype, encoding = mimetypes.guess_type(filename)
+        if ctype is None or encoding is not None:
+            # No guess could be made, or the file is encoded (compressed), so
+            # use a generic bag-of-bits type.
+            ctype = 'application/octet-stream'
+        maintype, subtype = ctype.split('/', 1)
+        if maintype == 'text':
+            fp = open(filename)
+            # Note: we should handle calculating the charset
+            attachment = MIMEText(fp.read(), _subtype=subtype)
+            fp.close()
+        elif maintype == 'image':
+            fp = open(filename, 'rb')
+            attachment = MIMEImage(fp.read(), _subtype=subtype)
+            fp.close()
+        elif maintype == 'audio':
+            fp = open(filename, 'rb')
+            attachment = MIMEAudio(fp.read(), _subtype=subtype)
+            fp.close()
+        else:
+            fp = open(filename, 'rb')
+            attachment = MIMEBase(maintype, subtype)
+            attachment.set_payload(fp.read())
+            fp.close()
+            # Encode the payload using Base64
+            encoders.encode_base64(attachment)
+        # Set the filename parameter
+        attachment.add_header('Content-Disposition', 'attachment', filename=filename)
+        outer.attach(attachment)
+    return outer
+
+def _status_msg(msg):
+    s = msg['State']
+    if s in annotation:
+        return '%s=%s' % (s, msg[annotation[s]])
+    else:
+        return s
+
+def _read_colors(ui):
+    colors = {}
+    # defaults
+    colors['new.color']             = 'red'
+    colors['new.on_color']          = 'on_grey'
+    colors['new.attrs']             = 'bold'
+    colors['resolved.color']        = 'white'
+    colors['resolved.on_color']     = ''
+    colors['resolved.attrs']        = ''
+    for v in colors:
+        colors[v] = ui.config('artemis', v, colors[v])
+        if v.endswith('attrs'): colors[v] = colors[v].split()
+    return colors
+
+def _color_summary(line, msg, colors):
+    if msg['State'] == 'new':
+        return colored(line, colors['new.color'],      attrs = colors['new.attrs'])
+    elif msg['State'] in state['fixed']:
+        return colored(line, colors['resolved.color'], attrs = colors['resolved.attrs'])
+    else:
+        return line
+
+def _summary_line(mbox, root, issue, colors):
+    line = "%s (%3d) [%s]: %s\n" % (issue,
+                                    len(mbox)-1,                # number of replies (-1 for self)
+                                    _status_msg(mbox[root]),
+                                    mbox[root]['Subject'])
+    return _color_summary(line, mbox[root], colors)
+
+cmdtable = {
+    'ilist':    (ilist,
+                 [('a', 'all', False,
+                   'list all issues (by default only those with state new)'),
+                  ('p', 'property', [],
+                   'list issues with specific field values (e.g., -p state=fixed); lists all possible values of a property if no = sign'),
+                  ('o', 'order', 'new', 'order of the issues; choices: "new" (date submitted), "latest" (date of the last message)'),
+                  ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
+                  ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (default_issues_dir, filter_prefix))],
+                 _('hg ilist [OPTIONS]')),
+    'iadd':       (iadd,
+                 [('a', 'attach', [],
+                   'attach file(s) (e.g., -a filename1 -a filename2)'),
+                  ('p', 'property', [],
+                   'update properties (e.g., -p state=fixed)'),
+                  ('n', 'no-property-comment', None,
+                   'do not add a comment about changed properties'),
+                  ('m', 'message', '',
+                   'use <text> as an issue subject'),
+                  ('c', 'commit', False,
+                   'perform a commit after the addition')],
+                 _('hg iadd [OPTIONS] [ID] [COMMENT]')),
+    'ishow':      (ishow,
+                 [('a', 'all', None, 'list all comments'),
+                  ('s', 'skip', '>', 'skip lines starting with a substring'),
+                  ('x', 'extract', [], 'extract attachments (provide attachment number as argument)'),
+                  ('', 'mutt', False, 'use mutt to show issue')],
+                 _('hg ishow [OPTIONS] ID [COMMENT]')),
+}
+
+# vim: expandtab
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/termcolor.py	Fri Apr 15 23:10:12 2011 -0700
@@ -0,0 +1,168 @@
+# coding: utf-8
+# Copyright (c) 2008-2011 Volvox Development Team
+#
+# 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.
+#
+# Author: Konstantin Lepa <konstantin.lepa@gmail.com>
+
+"""ANSII Color formatting for output in terminal."""
+
+from __future__ import print_function
+import os
+
+
+__ALL__ = [ 'colored', 'cprint' ]
+
+VERSION = (1, 1, 0)
+
+ATTRIBUTES = dict(
+        list(zip([
+            'bold',
+            'dark',
+            '',
+            'underline',
+            'blink',
+            '',
+            'reverse',
+            'concealed'
+            ],
+            list(range(1, 9))
+            ))
+        )
+del ATTRIBUTES['']
+
+
+HIGHLIGHTS = dict(
+        list(zip([
+            'on_grey',
+            'on_red',
+            'on_green',
+            'on_yellow',
+            'on_blue',
+            'on_magenta',
+            'on_cyan',
+            'on_white'
+            ],
+            list(range(40, 48))
+            ))
+        )
+
+
+COLORS = dict(
+        list(zip([
+            'grey',
+            'red',
+            'green',
+            'yellow',
+            'blue',
+            'magenta',
+            'cyan',
+            'white',
+            ],
+            list(range(30, 38))
+            ))
+        )
+
+
+RESET = '\033[0m'
+
+
+def colored(text, color=None, on_color=None, attrs=None):
+    """Colorize text.
+
+    Available text colors:
+        red, green, yellow, blue, magenta, cyan, white.
+
+    Available text highlights:
+        on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white.
+
+    Available attributes:
+        bold, dark, underline, blink, reverse, concealed.
+
+    Example:
+        colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink'])
+        colored('Hello, World!', 'green')
+    """
+    if os.getenv('ANSI_COLORS_DISABLED') is None:
+        fmt_str = '\033[%dm%s'
+        if color is not None:
+            text = fmt_str % (COLORS[color], text)
+
+        if on_color is not None:
+            text = fmt_str % (HIGHLIGHTS[on_color], text)
+
+        if attrs is not None:
+            for attr in attrs:
+                text = fmt_str % (ATTRIBUTES[attr], text)
+
+        text += RESET
+    return text
+
+
+def cprint(text, color=None, on_color=None, attrs=None, **kwargs):
+    """Print colorize text.
+
+    It accepts arguments of print function.
+    """
+
+    print((colored(text, color, on_color, attrs)), **kwargs)
+
+
+if __name__ == '__main__':
+    print('Current terminal type: %s' % os.getenv('TERM'))
+    print('Test basic colors:')
+    cprint('Grey color', 'grey')
+    cprint('Red color', 'red')
+    cprint('Green color', 'green')
+    cprint('Yellow color', 'yellow')
+    cprint('Blue color', 'blue')
+    cprint('Magenta color', 'magenta')
+    cprint('Cyan color', 'cyan')
+    cprint('White color', 'white')
+    print(('-' * 78))
+
+    print('Test highlights:')
+    cprint('On grey color', on_color='on_grey')
+    cprint('On red color', on_color='on_red')
+    cprint('On green color', on_color='on_green')
+    cprint('On yellow color', on_color='on_yellow')
+    cprint('On blue color', on_color='on_blue')
+    cprint('On magenta color', on_color='on_magenta')
+    cprint('On cyan color', on_color='on_cyan')
+    cprint('On white color', color='grey', on_color='on_white')
+    print('-' * 78)
+
+    print('Test attributes:')
+    cprint('Bold grey color', 'grey', attrs=['bold'])
+    cprint('Dark red color', 'red', attrs=['dark'])
+    cprint('Underline green color', 'green', attrs=['underline'])
+    cprint('Blink yellow color', 'yellow', attrs=['blink'])
+    cprint('Reversed blue color', 'blue', attrs=['reverse'])
+    cprint('Concealed Magenta color', 'magenta', attrs=['concealed'])
+    cprint('Bold underline reverse cyan color', 'cyan',
+            attrs=['bold', 'underline', 'reverse'])
+    cprint('Dark blink concealed white color', 'white',
+            attrs=['dark', 'blink', 'concealed'])
+    print(('-' * 78))
+
+    print('Test mixing:')
+    cprint('Underline red on grey color', 'red', 'on_grey',
+            ['underline'])
+    cprint('Reversed green on red color', 'green', 'on_red', ['reverse'])
+
--- a/termcolor.py	Fri Apr 15 22:57:55 2011 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-# coding: utf-8
-# Copyright (c) 2008-2011 Volvox Development Team
-#
-# 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.
-#
-# Author: Konstantin Lepa <konstantin.lepa@gmail.com>
-
-"""ANSII Color formatting for output in terminal."""
-
-from __future__ import print_function
-import os
-
-
-__ALL__ = [ 'colored', 'cprint' ]
-
-VERSION = (1, 1, 0)
-
-ATTRIBUTES = dict(
-        list(zip([
-            'bold',
-            'dark',
-            '',
-            'underline',
-            'blink',
-            '',
-            'reverse',
-            'concealed'
-            ],
-            list(range(1, 9))
-            ))
-        )
-del ATTRIBUTES['']
-
-
-HIGHLIGHTS = dict(
-        list(zip([
-            'on_grey',
-            'on_red',
-            'on_green',
-            'on_yellow',
-            'on_blue',
-            'on_magenta',
-            'on_cyan',
-            'on_white'
-            ],
-            list(range(40, 48))
-            ))
-        )
-
-
-COLORS = dict(
-        list(zip([
-            'grey',
-            'red',
-            'green',
-            'yellow',
-            'blue',
-            'magenta',
-            'cyan',
-            'white',
-            ],
-            list(range(30, 38))
-            ))
-        )
-
-
-RESET = '\033[0m'
-
-
-def colored(text, color=None, on_color=None, attrs=None):
-    """Colorize text.
-
-    Available text colors:
-        red, green, yellow, blue, magenta, cyan, white.
-
-    Available text highlights:
-        on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white.
-
-    Available attributes:
-        bold, dark, underline, blink, reverse, concealed.
-
-    Example:
-        colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink'])
-        colored('Hello, World!', 'green')
-    """
-    if os.getenv('ANSI_COLORS_DISABLED') is None:
-        fmt_str = '\033[%dm%s'
-        if color is not None:
-            text = fmt_str % (COLORS[color], text)
-
-        if on_color is not None:
-            text = fmt_str % (HIGHLIGHTS[on_color], text)
-
-        if attrs is not None:
-            for attr in attrs:
-                text = fmt_str % (ATTRIBUTES[attr], text)
-
-        text += RESET
-    return text
-
-
-def cprint(text, color=None, on_color=None, attrs=None, **kwargs):
-    """Print colorize text.
-
-    It accepts arguments of print function.
-    """
-
-    print((colored(text, color, on_color, attrs)), **kwargs)
-
-
-if __name__ == '__main__':
-    print('Current terminal type: %s' % os.getenv('TERM'))
-    print('Test basic colors:')
-    cprint('Grey color', 'grey')
-    cprint('Red color', 'red')
-    cprint('Green color', 'green')
-    cprint('Yellow color', 'yellow')
-    cprint('Blue color', 'blue')
-    cprint('Magenta color', 'magenta')
-    cprint('Cyan color', 'cyan')
-    cprint('White color', 'white')
-    print(('-' * 78))
-
-    print('Test highlights:')
-    cprint('On grey color', on_color='on_grey')
-    cprint('On red color', on_color='on_red')
-    cprint('On green color', on_color='on_green')
-    cprint('On yellow color', on_color='on_yellow')
-    cprint('On blue color', on_color='on_blue')
-    cprint('On magenta color', on_color='on_magenta')
-    cprint('On cyan color', on_color='on_cyan')
-    cprint('On white color', color='grey', on_color='on_white')
-    print('-' * 78)
-
-    print('Test attributes:')
-    cprint('Bold grey color', 'grey', attrs=['bold'])
-    cprint('Dark red color', 'red', attrs=['dark'])
-    cprint('Underline green color', 'green', attrs=['underline'])
-    cprint('Blink yellow color', 'yellow', attrs=['blink'])
-    cprint('Reversed blue color', 'blue', attrs=['reverse'])
-    cprint('Concealed Magenta color', 'magenta', attrs=['concealed'])
-    cprint('Bold underline reverse cyan color', 'cyan',
-            attrs=['bold', 'underline', 'reverse'])
-    cprint('Dark blink concealed white color', 'white',
-            attrs=['dark', 'blink', 'concealed'])
-    print(('-' * 78))
-
-    print('Test mixing:')
-    cprint('Underline red on grey color', 'red', 'on_grey',
-            ['underline'])
-    cprint('Reversed green on red color', 'green', 'on_red', ['reverse'])
-