# HG changeset patch # User David Anderson # Date 1237655527 0 # Node ID 8cfb054b73b2be67ff051f704ecf54e7acbad897 # Parent db7c985800083d0dc43412fa9e59d403440819cc Factor out input and file utils into io.py. diff -r db7c98580008 -r 8cfb054b73b2 scripts/release/error.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 diff -r db7c98580008 -r 8cfb054b73b2 scripts/release/io.py --- /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" ', + ] + +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)) diff -r db7c98580008 -r 8cfb054b73b2 scripts/release/release.py --- 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: