Factor out input and file utils into io.py.
authorDavid Anderson <david.jc.anderson@gmail.com>
Sat, 21 Mar 2009 17:12:07 +0000
changeset 1981 8cfb054b73b2
parent 1980 db7c98580008
child 1982 d3bebec883f1
Factor out input and file utils into io.py.
scripts/release/error.py
scripts/release/io.py
scripts/release/release.py
--- a/scripts/release/error.py	Sat Mar 21 16:19:42 2009 +0000
+++ b/scripts/release/error.py	Sat Mar 21 17:12:07 2009 +0000
@@ -31,3 +31,8 @@
 class ExpectationFailed(Error):
   """An unexpected state was encountered by an automated step."""
   pass
+
+
+class AbortedByUser(Error):
+  """The operation was aborted by the user."""
+  pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/release/io.py	Sat Mar 21 17:12:07 2009 +0000
@@ -0,0 +1,144 @@
+#!/usr/bin/python2.5
+#
+# Copyright 2009 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.
+
+from __future__ import with_statement
+
+"""User prompting and file access utilities."""
+
+__authors__ = [
+    # alphabetical order by last name, please
+    '"David Anderson" <dave@natulte.net>',
+    ]
+
+import error
+import log
+
+
+class Error(error.Error):
+  pass
+
+
+class FileAccessError(Error):
+  """An error occured while accessing a file."""
+  pass
+
+
+def getString(prompt):
+  """Prompt for and return a string."""
+  prompt += ' '
+  log.stdout.write(prompt)
+  log.stdout.flush()
+
+  response = sys.stdin.readline()
+  log.terminal_echo(prompt + response.strip())
+  if not response:
+    raise error.AbortedByUser('Aborted by ctrl+D')
+
+  return response.strip()
+
+
+def confirm(prompt, default=False):
+  """Ask a yes/no question and return the answer.
+
+  Will reprompt the user until one of "yes", "no", "y" or "n" is
+  entered. The input is case insensitive.
+
+  Args:
+    prompt: The question to ask the user.
+    default: The answer to return if the user just hits enter.
+
+  Returns:
+    True if the user answered affirmatively, False otherwise.
+  """
+  if default:
+    question = prompt + ' [Yn]'
+  else:
+    question = prompt + ' [yN]'
+  while True:
+    answer = getString(question)
+    if not answer:
+      return default
+    elif answer in ('y', 'yes'):
+      return True
+    elif answer in ('n', 'no'):
+      return False
+    else:
+      log.error('Please answer yes or no.')
+
+
+def getNumber(prompt):
+  """Prompt for and return a number.
+
+  Will reprompt the user until a number is entered.
+  """
+  while True:
+    value_str = getString(prompt)
+    try:
+      return int(value_str)
+    except ValueError:
+      log.error('Please enter a number. You entered "%s".' % value_str)
+
+
+def getChoice(intro, prompt, choices, done=None, suggest=None):
+  """Prompt for and return a choice from a menu.
+
+  Will reprompt the user until a valid menu entry is chosen.
+
+  Args:
+    intro: Text to print verbatim before the choice menu.
+    prompt: The prompt to print right before accepting input.
+    choices: The list of string choices to display.
+    done: If not None, the list of indices of previously
+      selected/completed choices.
+    suggest: If not None, the index of the choice to highlight as
+      the suggested choice.
+
+  Returns:
+    The index in the choices list of the selection the user made.
+  """
+  done = set(done or [])
+  while True:
+    print intro
+    print
+    for i, entry in enumerate(choices):
+      done_text = ' (done)' if i in done else ''
+      indent = '--> ' if i == suggest else '    '
+      print '%s%2d. %s%s' % (indent, i+1, entry, done_text)
+    print
+    choice = getNumber(prompt)
+    if 0 < choice <= len(choices):
+      return choice-1
+    log.error('%d is not a valid choice between %d and %d' %
+              (choice, 1, len(choices)))
+    print
+
+
+def fileToLines(path):
+  """Read a file and return it as a list of lines."""
+  try:
+    with file(path) as f:
+      return f.read().split('\n')
+  except (IOError, OSError), e:
+    raise FileAccessError(str(e))
+
+
+def linesToFile(path, lines):
+  """Write a list of lines to a file."""
+  try:
+    with file(path, 'w') as f:
+      f.write('\n'.join(lines))
+  except (IOError, OSError), e:
+    raise FileAccessError(str(e))
--- a/scripts/release/release.py	Sat Mar 21 16:19:42 2009 +0000
+++ b/scripts/release/release.py	Sat Mar 21 17:12:07 2009 +0000
@@ -19,21 +19,16 @@
 """Google Summer of Code Melange release script.
 
 This script provides automation for the various tasks involved in
-pushing a new release of Melange to the official Google Summer of Code
-app engine instance.
+releasing a new version of Melange and pushing it to various app
+engine instances.
 
 It does not provide a turnkey autopilot solution. Notably, each stage
 of the release process must be started by a human operator, and some
 commands will request confirmation or extra details before
-proceeding. It is not a replacement for a cautious human
-operator.
+proceeding. It is not a replacement for a cautious human operator.
 
-Note that this script requires:
- - Python 2.5 or better (for various language features)
-
- - Subversion 1.5.0 or better (for working copy depth control, which
-     cuts down checkout/update times by several orders of
-     magnitude).
+Note that this script requires Python 2.5 or better (for various
+language features)
 """
 
 __authors__ = [
@@ -49,6 +44,7 @@
 import sys
 
 import error
+import io
 import log
 import subversion
 import util
@@ -57,7 +53,6 @@
 # Default repository URLs for Melange and the Google release
 # repository.
 MELANGE_REPOS = 'http://soc.googlecode.com/svn'
-GOOGLE_SOC_REPOS = 'https://soc-google.googlecode.com/svn'
 
 
 # Regular expression matching an apparently well formed Melange
@@ -69,124 +64,6 @@
   pass
 
 
-class AbortedByUser(Error):
-  """The operation was aborted by the user."""
-  pass
-
-
-class FileAccessError(Error):
-  """An error occured while accessing a file."""
-  pass
-
-
-def getString(prompt):
-  """Prompt for and return a string."""
-  prompt += ' '
-  log.stdout.write(prompt)
-  log.stdout.flush()
-
-  response = sys.stdin.readline()
-  log.terminal_echo(prompt + response.strip())
-  if not response:
-    raise AbortedByUser('Aborted by ctrl+D')
-
-  return response.strip()
-
-
-def confirm(prompt, default=False):
-  """Ask a yes/no question and return the answer.
-
-  Will reprompt the user until one of "yes", "no", "y" or "n" is
-  entered. The input is case insensitive.
-
-  Args:
-    prompt: The question to ask the user.
-    default: The answer to return if the user just hits enter.
-
-  Returns:
-    True if the user answered affirmatively, False otherwise.
-  """
-  if default:
-    question = prompt + ' [Yn]'
-  else:
-    question = prompt + ' [yN]'
-  while True:
-    answer = getString(question)
-    if not answer:
-      return default
-    elif answer in ('y', 'yes'):
-      return True
-    elif answer in ('n', 'no'):
-      return False
-    else:
-      log.error('Please answer yes or no.')
-
-
-def getNumber(prompt):
-  """Prompt for and return a number.
-
-  Will reprompt the user until a number is entered.
-  """
-  while True:
-    value_str = getString(prompt)
-    try:
-      return int(value_str)
-    except ValueError:
-      log.error('Please enter a number. You entered "%s".' % value_str)
-
-
-def getChoice(intro, prompt, choices, done=None, suggest=None):
-  """Prompt for and return a choice from a menu.
-
-  Will reprompt the user until a valid menu entry is chosen.
-
-  Args:
-    intro: Text to print verbatim before the choice menu.
-    prompt: The prompt to print right before accepting input.
-    choices: The list of string choices to display.
-    done: If not None, the list of indices of previously
-      selected/completed choices.
-    suggest: If not None, the index of the choice to highlight as
-      the suggested choice.
-
-  Returns:
-    The index in the choices list of the selection the user made.
-  """
-  done = set(done or [])
-  while True:
-    print intro
-    print
-    for i, entry in enumerate(choices):
-      done_text = ' (done)' if i in done else ''
-      indent = '--> ' if i == suggest else '    '
-      print '%s%2d. %s%s' % (indent, i+1, entry, done_text)
-    print
-    choice = getNumber(prompt)
-    if 0 < choice <= len(choices):
-      return choice-1
-    log.error('%d is not a valid choice between %d and %d' %
-              (choice, 1, len(choices)))
-    print
-
-
-def fileToLines(path):
-  """Read a file and return it as a list of lines."""
-  try:
-    with file(path) as f:
-      return f.read().split('\n')
-  except (IOError, OSError), e:
-    raise FileAccessError(str(e))
-
-
-def linesToFile(path, lines):
-  """Write a list of lines to a file."""
-  try:
-    with file(path, 'w') as f:
-      f.write('\n'.join(lines))
-  except (IOError, OSError), e:
-    raise FileAccessError(str(e))
-
-
 #
 # Decorators for use in ReleaseEnvironment.
 #
@@ -243,7 +120,7 @@
       self.wc.revert()
 
       if self.exists(self.BRANCH_FILE):
-        branch = fileToLines(self.path(self.BRANCH_FILE))[0]
+        branch = io.fileToLines(self.path(self.BRANCH_FILE))[0]
         self._switchBranch(branch)
       else:
         self._switchBranch(None)
@@ -312,7 +189,7 @@
     else:
       self.wc.update()
       assert self.wc.exists('branches/' + release)
-      linesToFile(self.path(self.BRANCH_FILE), [release])
+      io.linesToFile(self.path(self.BRANCH_FILE), [release])
       self.branch = release
       self.branch_dir = 'branches/' + release
       self.wc.update(self.branch_dir, depth='infinity')
@@ -340,10 +217,10 @@
       raise error.ExpectationFailed(
         'No branches available. Please import one.')
 
-    choice = getChoice('Available release branches:',
-               'Your choice?',
-               branches,
-               suggest=len(branches)-1)
+    choice = io.getChoice('Available release branches:',
+                          'Your choice?',
+                          branches,
+                          suggest=len(branches)-1)
     self._switchBranch(branches[choice])
 
   def _addAppYaml(self):
@@ -359,7 +236,7 @@
     yaml_path = self._branchPath('app/app.yaml')
     self.wc.copy(yaml_path + '.template', yaml_path)
 
-    yaml = fileToLines(self.wc.path(yaml_path))
+    yaml = io.fileToLines(self.wc.path(yaml_path))
     out = []
     for i, line in enumerate(yaml):
       stripped_line = line.strip()
@@ -372,7 +249,7 @@
         out.append('# * initial Google fork of Melange ' + self.branch)
       else:
         out.append(line)
-    linesToFile(self.wc.path(yaml_path), out)
+    io.linesToFile(self.wc.path(yaml_path), out)
 
     self.wc.commit('Create app.yaml with Google patch version g0 '
              'in branch ' + self.branch)
@@ -386,7 +263,7 @@
     # of the Melange codebase instead of the vanilla release.
     tmpl_file = self.wc.path(
       self._branchPath('app/soc/templates/soc/base.html'))
-    tmpl = fileToLines(tmpl_file)
+    tmpl = io.fileToLines(tmpl_file)
     for i, line in enumerate(tmpl):
       if 'http://code.google.com/p/soc/source/browse/tags/' in line:
         tmpl[i] = line.replace('/p/soc/', '/p/soc-google/')
@@ -394,7 +271,7 @@
     else:
       raise error.ExpectationFailed(
         'No source code link found in base.html')
-    linesToFile(tmpl_file, tmpl)
+    io.linesToFile(tmpl_file, tmpl)
 
     self.wc.commit(
       'Customize the Melange release link in the sidebar menu')
@@ -402,9 +279,9 @@
   @pristine_wc
   def importTag(self):
     """Import a new Melange release"""
-    release = getString('Enter the Melange release to import:')
+    release = io.getString('Enter the Melange release to import:')
     if not release:
-      AbortedByUser('No release provided, import aborted')
+      error.AbortedByUser('No release provided, import aborted')
 
     branch_dir = 'branches/' + release
     if self.wc.exists(branch_dir):
@@ -413,8 +290,8 @@
     tag_url = '%s/tags/%s' % (self.upstream_repos, release)
     release_rev = subversion.find_tag_rev(tag_url)
 
-    if confirm('Confirm import of release %s, tagged at r%d?' %
-           (release, release_rev)):
+    if io.confirm('Confirm import of release %s, tagged at r%d?' %
+                  (release, release_rev)):
       # Add an entry to the vendor externals for the Melange
       # release.
       externals = self.wc.propget('svn:externals', 'vendor/soc')
@@ -443,8 +320,8 @@
   @pristine_wc
   def cherryPickChange(self):
     """Cherry-pick a change from the Melange trunk"""
-    rev = getNumber('Revision number to cherry-pick:')
-    bug = getNumber('Issue fixed by this change:')
+    rev = io.getNumber('Revision number to cherry-pick:')
+    bug = io.getNumber('Issue fixed by this change:')
 
     diff = subversion.diff(self.upstream_repos + '/trunk', rev)
     if not diff.strip():
@@ -457,7 +334,7 @@
     yaml_path = self.wc.path(self._branchPath('app/app.yaml'))
     out = []
     updated_patchlevel = False
-    for line in fileToLines(yaml_path):
+    for line in io.fileToLines(yaml_path):
       if line.strip().startswith('version: '):
         version = line.strip().split()[-1]
         base, patch = line.rsplit('g', 1)
@@ -474,12 +351,12 @@
       log.error('Failed to update Google patch revision')
       log.error('Cherry-picking failed')
 
-    linesToFile(yaml_path, out)
+    io.linesToFile(yaml_path, out)
 
     log.info('Check the diff about to be committed with:')
     log.info('svn diff ' + self.wc.path(self.branch_dir))
-    if not confirm('Commit this change?'):
-      raise AbortedByUser('Cherry-pick aborted')
+    if not io.confirm('Commit this change?'):
+      raise error.AbortedByUser('Cherry-pick aborted')
     self.wc.commit(message)
     log.info('Cherry-picked r%d from the Melange trunk.' % rev)
 
@@ -516,9 +393,10 @@
         self.MENU_SUGGESTIONS[last_command])
 
       try:
-        choice = getChoice('Main menu:', 'Your choice?',
-          self.MENU_STRINGS, done=done, suggest=suggested_next)
-      except (KeyboardInterrupt, AbortedByUser):
+        choice = io.getChoice('Main menu:', 'Your choice?',
+                              self.MENU_STRINGS, done=done,
+                              suggest=suggested_next)
+      except (KeyboardInterrupt, error.AbortedByUser):
         log.info('Exiting.')
         return
       try: