#!/usr/bin/python2.5
#
# Copyright 2008 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Custom optparse OptionParser and functions for reading Python settings files.

Default values for trunk/scripts flags can be specified in valid Python syntax
in the ~/.soc_scripts_settings file.  For example, a default value for the
--user flag can be specified with a variable assignment in the settings file
like:

  user = 'joeuser'

Defaults in the ~/.soc_scripts_settings file can be explicitly overridden by
supplied the actual flag.  For example, supplying:

  --user=someotheruser

would override the default value present in the settings file.

  Option:  class derived from optparse.Option that adds a 'required' parameter
  OptionParser:  class derived from optparse.OptionParser for use with Option

  readPythonSettings():  interprets a valid Python file as a settings file
"""

__authors__ = [
  # alphabetical order by last name, please
  '"Todd Larsen" <tlarsen@google.com>',
]


import os
import optparse
import sys


DEF_SETTINGS_FILE_DIR = "~"
DEF_SETTINGS_FILE_NAME = '.soc_scripts_settings'


class Error(Exception):
  """Base exception class for all exceptions in the settings module."""
  pass


class Option(optparse.Option):
  """Class derived from optparse.Option that adds a 'required' parameter."""

  ATTRS = optparse.Option.ATTRS + ['required']

  def _check_required(self):
    """Insures that 'required' option can accept a value."""
    if self.required and (not self.takes_value()):
      raise optparse.OptionError(
          "'required' parameter set for option that does not take a value",
          self)

  # Make sure _check_required() is called from the constructor!
  CHECK_METHODS = optparse.Option.CHECK_METHODS + [_check_required]

  def process(self, opt, value, values, parser):
    optparse.Option.process(self, opt, value, values, parser)
    parser.option_seen[self] = 1


class OptionParser(optparse.OptionParser):
  """Class derived from optparse.OptionParser for use with Option."""

  def _init_parsing_state(self):
    """Sets up dict to track options seen so far."""
    optparse.OptionParser._init_parsing_state(self)
    self.option_seen = {}

  def error(self, *args):
    """Convert errors reported by optparse.OptionParser to Error exceptions.

    Args:
      *args:  passed through to the Error exception __init__() constructor,
        usually a list of strings

    Raises:
      Error with the supplied *args
    """
    raise Error(*args)

  def check_values(self, values, args):
    """Checks to make sure all required=True options were supplied.

    Args:
      values, args:  passed through unchanged (see Returns:)

    Returns:
      (values, args) unchanged.

    Raises:
      Error if an option was not supplied that had required=True;  exception
      positional arguments are the error message strings.
    """
    errors = []

    for option in self.option_list:
      if (isinstance(option, Option)
          and option.required
          and (not self.option_seen.has_key(option))):
        errors.append(
            'required %s option not supplied'
            ' (and default settings not allowed)' % option)

    if errors:
      self.error(*errors)

    return values, args


def printErrors(errors, exit_code=1):
  """Prints error message strings to sys.stderr and returns an exit code.

  Args:
    errors: error message string or list of error message strings to be printed
      to sys.stderr
    exit_code: exit code to return (so that this function can be used as an
      expression in sys.exit() for example); default is 1

  Returns:
    exit_code
  """
  sys.stderr.write('\nERRORS:\n')

  if (not isinstance(errors, tuple)) and (not isinstance(errors, list)):
    errors = [errors]

  for msg in errors:
    sys.stderr.write('  %s\n' % msg)

  sys.stderr.write('\n')

  return exit_code


def printErrorsAndUsage(errors, parser, exit_code=1):
  """Prints error messages and usage text to sys.stderr and returns exit code.

  Args:
    errors: error message string or list of error message strings to be printed
      to sys.stderr
    parser: OptionParser with a print_help() method
    exit_code: exit code to return (so that this function can be used as an
      expression in sys.exit() for example); default is 1

  Returns:
    exit_code
  """
  exit_code = printErrors(errors, exit_code=exit_code)
  parser.print_help(file=sys.stderr)

  return exit_code


def getExpandedPath(path):
  """Returns an expanded, normalized, absolute path.

  Args:
    path: path (possibly relative, possibly containing environment variables,
      etc.) to be expanded, normalized and made absolute

  Returns:
    absolute path, after expanding any environment variables and "~", then
    removing excess . and .. path elements
  """
  return os.path.abspath(
      os.path.normpath(
          os.path.expanduser(
              os.path.expandvars(path))))


def readPythonSettings(defaults={},  # {} OK since defaults is always copied
                       settings_dir=DEF_SETTINGS_FILE_DIR,
                       settings_file=DEF_SETTINGS_FILE_NAME):
  """Executes a Python-syntax settings file and returns the local variables.

  Args:
    defaults:  dict of default values to use when settings are not present
      in the settings file (or if no settings file is present at all);  this
      dict is *copied* and is not altered at all
    settings_dir:  optional directory containing settings_file
    settings_file:  optional settings file name found in settings_dir

  Returns:
    dict of setting name/value pairs (possibly including some values from the
    defaults parameter).  Since the settings file is full-fledged Python
    source, the values could be any valid Python object.

  Raises:
    Error if some error occurred parsing the present settings file;  exception
    positional arguments are the error message strings.
  """
  # do not let the original defaults be altered
  defaults = defaults.copy()

  # form absolute path to the settings file, expanding any environment
  # variables and "~", then removing excess . and .. path elements
  path = getExpandedPath(os.path.join(settings_dir, settings_file))

  # empty dict to capture the local variables in the settings file
  settings_locals = {}

  try:
    # execute the Python source file and recover the local variables as settings
    execfile(path, {}, settings_locals)
  except IOError:
    # If the settings file is not present, there are no defaults.
    pass
  except Exception, error:
    # Other exceptions usually mean a faulty settings file.
    raise Error(
        'faulty settings file:',
        ('  %s: %s' % (error.__class__.__name__, str(error))),
        ('  %s' % path))

  # overwrite defaults copy with values from the (possibly empty) settings file
  defaults.update(settings_locals)

  return defaults


def readPythonSettingsOrDie(parser=None, **kwargs):
  """Calls readPythonSettings(), calling sys.exit() on any errors.

  Args:
    parser: if supplied, an OptionParser instance used to call print_help()
      to print usage information if errors occur
    **kwargs: see readPythonSettings()

  Returns:
    On success, returns results of readPythonSettings().

  Exits:
    On any error from readPythonSettings(), prints error messages to stderr,
    possibly prints usage information, and calls sys.exit(1).
  """
  try:
    return readPythonSettings(**kwargs)
  except Error, error:
    if parser:
      sys.exit(printErrorsAndUsage(error.args, parser))
    else:
      sys.exit(printErrors(error.args))


def makeOptionParserOrDie(*args, **kwargs):
  """Creates and returns an OptionParser, calling sys.exit() on any errors.

  Args:
    *args, **kwargs: supplied directly to OptionParser constructor

  Returns:
    On success, returns an OptionParser instance.

  Exits:
    On any error, prints error messages to stderr and calls sys.exit(1).
  """
  try:
    return OptionParser(*args, **kwargs)
  except Error, error:
    sys.exit(printErrors(error.args))


def parseOptionsOrDie(parser, args):
  """Parses command-line options, calling sys.exit() on any errors.

  Args:
    parser: an OptionParser instance
    args: list of command-line arguments to supply to parser

  Returns:
    On success, returns (options, args) returned by parser.parse_args(args).

  Exits:
    On any error, prints error messages and usage information to stderr and
    calls sys.exit(1).
  """
  try:
    return parser.parse_args(args)
  except Error, error:
    sys.exit(printErrorsAndUsage(error.args, parser))


def checkCommonSvnOptions(options):
  """Checks a common subset of command-line options.

  Multiple scripts accept a subset of common command-line options.  This
  function does some sanity checks on these flags.  These checks are collected
  here because they were being duplicated in multiple scripts.

  Args:
    options: OptionParser.parse_args() options instance to check

  Returns:
    list of error message strings, or an empty list if no errors
  """
  errors = []

  if not options.repo:
    errors.extend(
        ['--repo must be supplied or have a settings file default'])

  if not options.wc:
    errors.extend(
        ['--wc must be supplied or have a settings file default'])

  if not options.branch:
    if not options.user:
      errors.extend(
          ['at least one of --branch or --user must be supplied'])

  return errors


def checkCommonSvnOptionsOrDie(options, parser):
  """Checks subset of command-line options, calling sys.exit() on any errors.

  Args:
    options: see checkCommonSvnOptions()
    parser: an OptionParser instance used to call print_help() to print
      usage information if errors occur

  Exits:
    On any error messages returned by checkCommonSvnOptions(), prints error
    messages and usage information to stderr and calls sys.exit(1).
  """
  errors = checkCommonSvnOptions(options)

  if errors:
    sys.exit(printErrorsAndUsage(errors, parser))
