#! /usr/bin/env python

"""
:Author: David Goodger
:Contact: goodger@users.sourceforge.net
:Revision: $Revision: 1.18 $
:Date: $Date: 2002/07/31 02:13:22 $
:Copyright: This module has been placed in the public domain.

Command-line and common processing for Docutils front-end tools.

Exports the following classes:

- `OptionParser`: Standard Docutils command-line processing.
- `Values`: Option values; objects are simple structs (``object.attribute``).
- `ConfigParser`: Standard Docutils config file processing.
"""

__docformat__ = 'reStructuredText'

import os
import os.path
import ConfigParser as CP
import docutils
from docutils import optik
from docutils.optik import Values


def store_multiple(option, opt, value, parser, *args, **kwargs):
    """
    Store multiple values in `parser.values`.  (Option callback.)
    
    Store `None` for each attribute named in `args`, and store the value for
    each key (attribute name) in `kwargs`.
    """
    for attribute in args:
        setattr(parser.values, attribute, None)
    for key, value in kwargs.items():
        setattr(parser.values, key, value)

def read_config_file(option, opt, value, parser):
    """
    Read a configuration file during option processing.  (Option callback.)
    """
    config_parser = ConfigParser()
    config_parser.read(value)
    settings = config_parser.get_section('options')
    make_paths_absolute(settings, os.path.dirname(value))
    parser.values.__dict__.update(settings)

relative_path_options = ('warning_stream', 'stylesheet', 'pep_stylesheet',
                         'pep_template')

def make_paths_absolute(dictionary, base_path=None):
    """
    Interpret filesystem path settings relative to the `base_path` given.
    """
    if base_path is None:
        base_path = os.getcwd()
    for option in relative_path_options:
        if dictionary.has_key(option) and dictionary[option]:
            dictionary[option] = os.path.normpath(
                os.path.join(base_path, dictionary[option]))


class OptionParser(optik.OptionParser):

    """
    Parser for command-line and library use.  The `cmdline_options`
    specification here and in other Docutils components are merged to
    build the set of command-line options for this process.

    Common options (defined below) and component-specific options must
    not conflict. Short options are reserved for common options, and
    components are restrict to using long options.
    """

    threshold_choices = 'info 1 warning 2 error 3 severe 4 none 5'.split()
    """Possible inputs for for --report and --halt threshold values."""

    thresholds = {'info': 1, 'warning': 2, 'error': 3, 'severe': 4, 'none': 5}
    """Lookup table for --report and --halt threshold values."""

    cmdline_options = (
        'General Docutils Options',
        None,
        (('Include a "Generated by Docutils" credit and link at the end '
          'of the document.',
          ['--generator', '-g'], {'action': 'store_true'}),
         ('Do not include a generator credit.',
          ['--no-generator'], {'action': 'store_false', 'dest': 'generator'}),
         ('Include the date at the end of the document (UTC).',
          ['--date', '-d'], {'action': 'store_const', 'const': '%Y-%m-%d',
                             'dest': 'datestamp'}),
         ('Include the time & date at the end of the document (UTC).',
          ['--time', '-t'], {'action': 'store_const',
                             'const': '%Y-%m-%d %H:%M UTC',
                             'dest': 'datestamp'}),
         ('Do not include a datestamp of any kind.',
          ['--no-datestamp'], {'action': 'store_const', 'const': None,
                               'dest': 'datestamp'}),
         ('Include a "View document source" link (relative to destination).',
          ['--source-link', '-s'], {'action': 'store_true'}),
         ('Use the supplied <url> for a "View document source" link; '
          'implies --source-link.',
          ['--source-url'], {'metavar': '<url>'}),
         ('Do not include a "View document source" link.',
          ['--no-source-link'],
          {'action': 'callback', 'callback': store_multiple,
           'callback_args': ('source_link', 'source_url')}),
         ('Enable backlinks from section headers to table of contents '
          'entries.  This is the default.',
          ['--toc-entry-backlinks'],
          {'dest': 'toc_backlinks', 'action': 'store_const', 'const': 'entry',
           'default': 'entry'}),
         ('Enable backlinks from section headers to the top of the table of '
          'contents.',
          ['--toc-top-backlinks'],
          {'dest': 'toc_backlinks', 'action': 'store_const', 'const': 'top'}),
         ('Disable backlinks to the table of contents.',
          ['--no-toc-backlinks'],
          {'dest': 'toc_backlinks', 'action': 'store_false'}),
         ('Enable backlinks from footnotes and citations to their '
          'references.  This is the default.',
          ['--footnote-backlinks'],
          {'action': 'store_true', 'default': 1}),
         ('Disable backlinks from footnotes and citations.',
          ['--no-footnote-backlinks'],
          {'dest': 'footnote_backlinks', 'action': 'store_false'}),
         ('Set verbosity threshold; report system messages at or higher than '
          '<level> (by name or number: "info" or "1", warning/2, error/3, '
          'severe/4; also, "none" or "5").  Default is 2 (warning).',
          ['--report', '-r'], {'choices': threshold_choices, 'default': 2,
                               'dest': 'report_level', 'metavar': '<level>'}),
         ('Report all system messages, info-level and higher.  (Same as '
          '"--report=info".)',
          ['--verbose', '-v'], {'action': 'store_const', 'const': 'info',
                                'dest': 'report_level'}),
         ('Do not report any system messages.  (Same as "--report=none".)',
          ['--quiet', '-q'], {'action': 'store_const', 'const': 'none',
                              'dest': 'report_level'}),
         ('Set the threshold (<level>) at or above which system messages are '
          'converted to exceptions, halting execution immediately.  Levels '
          'as in --report.  Default is 4 (severe).',
          ['--halt'], {'choices': threshold_choices, 'dest': 'halt_level',
                       'default': 4, 'metavar': '<level>'}),
         ('Same as "--halt=info": halt processing at the slightest problem.',
          ['--strict'], {'action': 'store_const', 'const': 'info',
                         'dest': 'halt_level'}),
         ('Report debug-level system messages.',
          ['--debug'], {'action': 'store_true'}),
         ('Do not report debug-level system messages.',
          ['--no-debug'], {'action': 'store_false', 'dest': 'debug'}),
         ('Send the output of system messages (warnings) to <file>.',
          ['--warnings'], {'dest': 'warning_stream', 'metavar': '<file>'}),
         ('Specify the encoding of input text.  Default is locale-dependent.',
          ['--input-encoding', '-i'], {'metavar': '<name>'}),
         ('Specify the encoding for output.  Default is UTF-8.',
          ['--output-encoding', '-o'],
          {'metavar': '<name>', 'default': 'utf-8'}),
         ('Specify the language of input text (ISO 639 2-letter identifier).'
          '  Default is "en" (English).',
          ['--language', '-l'], {'dest': 'language_code', 'default': 'en',
                                 'metavar': '<name>'}),
         ('Read configuration options from <file>, if it exists.',
          ['--config'], {'metavar': '<file>', 'type': 'string',
                         'action': 'callback', 'callback': read_config_file}),
         ("Show this program's version number and exit.",
          ['--version'], {'action': 'version'}),
         ('Show this help message and exit.',
          ['--help', '-h'], {'action': 'help'}),
         # Hidden options, for development use only:
         (optik.SUPPRESS_HELP,
          ['--dump-internals'],
          {'action': 'store_true'}),))
    """Command-line option specifications, common to all Docutils front ends.
    One or more sets of option group title, description, and a list/tuple of
    tuples: ``('help text', [list of option strings], {keyword arguments})``.
    Group title and/or description may be `None`; no group title implies no
    group, just a list of single options.  Option specs from Docutils
    components are also used (see `populate_from_components()`)."""

    version_template = '%%prog (Docutils %s)' % docutils.__version__

    def __init__(self, components=(), *args, **kwargs):
        """
        `components` is a list of Docutils components each containing a
        ``.cmdline_options`` attribute.  `defaults` is a mapping of option
        default overrides.
        """
        optik.OptionParser.__init__(self, help=None, format=optik.Titled(),
                                    *args, **kwargs)
        if not self.version:
            self.version = self.version_template
        self.populate_from_components(tuple(components) + (self,))

    def populate_from_components(self, components):
        for component in components:
            if component is not None:
                i = 0
                cmdline_options = component.cmdline_options
                while i < len(cmdline_options):
                    title, description, option_spec = cmdline_options[i:i+3]
                    if title:
                        group = optik.OptionGroup(self, title, description)
                        self.add_option_group(group)
                    else:
                        group = self        # single options
                    for (help_text, option_strings, kwargs) in option_spec:
                        group.add_option(help=help_text, *option_strings,
                                         **kwargs)
                    i += 3

    def check_values(self, values, args):
        if hasattr(values, 'report_level'):
            values.report_level = self.check_threshold(values.report_level)
        if hasattr(values, 'halt_level'):
            values.halt_level = self.check_threshold(values.halt_level)
        values._source, values._destination = self.check_args(args)
        make_paths_absolute(values.__dict__, os.getcwd())
        return values

    def check_threshold(self, level):
        try:
            return int(level)
        except ValueError:
            try:
                return self.thresholds[level.lower()]
            except (KeyError, AttributeError):
                self.error('Unknown threshold: %r.' % level)

    def check_args(self, args):
        source = destination = None
        if args:
            source = args.pop(0)
        if args:
            destination = args.pop(0)
        if args:
            self.error('Maximum 2 arguments allowed.')
        return source, destination


class ConfigParser(CP.ConfigParser):

    standard_config_files = (
        '/etc/docutils.conf',               # system-wide
        './docutils.conf',                  # project-specific
        os.path.expanduser('~/.docutils'))  # user-specific
    """Docutils configuration files, using ConfigParser syntax (section
    'options').  Later files override earlier ones."""

    def read_standard_files(self):
        self.read(self.standard_config_files)

    def optionxform(self, optionstr):
        """
        Transform '-' to '_' so the cmdline form of option names can be used.
        """
        return optionstr.lower().replace('-', '_')

    def get_section(self, section, raw=0, vars=None):
        """
        Return a given section as a dictionary (empty if the section
        doesn't exist).

        All % interpolations are expanded in the return values, based on the
        defaults passed into the constructor, unless the optional argument
        `raw` is true.  Additional substitutions may be provided using the
        `vars` argument, which must be a dictionary whose contents overrides
        any pre-existing defaults.

        The section DEFAULT is special.
        """
        try:
            sectdict = self._ConfigParser__sections[section].copy()
        except KeyError:
            sectdict = {}
        d = self._ConfigParser__defaults.copy()
        d.update(sectdict)
        # Update with the entry specific variables
        if vars:
            d.update(vars)
        if raw:
            return sectdict
        # do the string interpolation
        for option in sectdict.keys():
            rawval = sectdict[option]
            value = rawval              # Make it a pretty variable name
            depth = 0
            while depth < 10:           # Loop through this until it's done
                depth += 1
                if value.find("%(") >= 0:
                    try:
                        value = value % d
                    except KeyError, key:
                        raise CP.InterpolationError(key, option, section,
                                                    rawval)
                else:
                    break
            if value.find("%(") >= 0:
                raise CP.InterpolationDepthError(option, section, rawval)
            sectdict[option] = value
        return sectdict
