scripts/settings.py
author Mario Ferraro <fadinlight@gmail.com>
Sun, 15 Nov 2009 22:12:20 +0100
changeset 3093 d1be59b6b627
parent 49 7b6914018044
permissions -rw-r--r--
GMaps related JS changed to use new google namespace. Google is going to change permanently in the future the way to load its services, so better stay safe. Also this commit shows uses of the new melange.js module. Fixes Issue 634.

#!/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))