jenkins-bot has submitted this change and it was merged.
Change subject: Use consistent Exception for user selecting quit ......................................................................
Use consistent Exception for user selecting quit
Add a QuitKeyboardInterrupt as subclass of KeyboardInterrupt
Add new methods to pywikibot.Bot to: - quit(): raises QuitKeyboardInterrupt - run(): if self.generator is defined, call treat() for each page
Split pywikbot.Bot.userPut to create method userConfirm(), which calls self.quit() if user enters 'q'.
Display edit summary to be used in userPut().
Alter scripts which allowed input of 'q' to quit, subclassing them to pywikibot.Bot, and using self.userPut() or self.quit() to cascade the break out of processing pages.
Fixes bug: 68663 misspelling doesnt quit on user input 'q'
Change-Id: I84cdc5dcb8626d2cc1d18f963a8d6fdf4c99da6a --- M pywikibot/bot.py M scripts/add_text.py M scripts/capitalize_redirects.py M scripts/catall.py M scripts/clean_sandbox.py M scripts/commonscat.py M scripts/cosmetic_changes.py M scripts/movepages.py M scripts/redirect.py M scripts/replace.py M scripts/solve_disambiguation.py M scripts/unlink.py M tests/script_tests.py 13 files changed, 140 insertions(+), 159 deletions(-)
Approvals: John Vandenberg: Looks good to me, approved XZise: Looks good to me, but someone else must approve jenkins-bot: Verified
diff --git a/pywikibot/bot.py b/pywikibot/bot.py index 41cdb39..b5474eb 100644 --- a/pywikibot/bot.py +++ b/pywikibot/bot.py @@ -766,10 +766,25 @@ pywikibot.stdout(globalHelp)
+class QuitKeyboardInterrupt(KeyboardInterrupt): + + """The user has cancelled processing at a prompt.""" + + class Bot(object):
""" - Generic Bot to be subclassed + Generic Bot to be subclassed. + + This class provides a run() method for basic processing of a + generator one page at a time. + + If the subclass places a page generator in self.generator, + Bot will process each page in the generator, invoking the method treat() + which must then be implemented by subclasses. + + If the subclass does not set a generator, or does not override + treat() or run(), NotImplementedError is raised. """
# Bot configuration. @@ -820,8 +835,30 @@ except KeyError: raise pywikibot.Error(u'%s is not a valid bot option.' % option)
+ def user_confirm(self, question): + """ Obtain user response if bot option 'always' not enabled. """ + if self.getOption('always'): + return True + + choice = pywikibot.inputChoice(question, + ['Yes', 'No', 'All', 'Quit'], + ['y', 'N', 'a', 'q'], 'N') + if choice == 'n': + return False + + if choice == 'q': + self.quit() + + if choice == 'a': + # Remember the choice + self.options['always'] = True + + return True + def userPut(self, page, oldtext, newtext, **kwargs): """ + Save a new revision of a page, with user confirmation as required. + Print differences, ask user for confirmation, and puts the page if needed.
@@ -836,25 +873,48 @@ pywikibot.output(u"\n\n>>> \03{lightpurple}%s\03{default} <<<" % page.title()) pywikibot.showDiff(oldtext, newtext) + if 'comment' in kwargs: + pywikibot.output(u'Comment: %s' % kwargs['comment'])
- choice = 'a' - if not self.getOption('always'): - choice = pywikibot.inputChoice( - u'Do you want to accept these changes?', - ['Yes', 'No', 'All'], - ['y', 'N', 'a'], - 'N' - ) - if choice == 'a': - # Remember the choice - self.options['always'] = True + if not self.user_confirm('Do you want to accept these changes?'): + return
- if 'async' not in kwargs: - kwargs['async'] = (choice == 'a') + if 'async' not in kwargs and self.options['always']: + kwargs['async'] = True
- if choice != 'n': - page.text = newtext - page.save(**kwargs) + page.text = newtext + page.save(**kwargs) + + def quit(self): + """Cleanup and quit processing.""" + raise QuitKeyboardInterrupt + + def treat(self, page): + """Process one page (Abstract method).""" + raise NotImplementedError('Method %s.treat() not implemented.' + % self.__class__.__name__) + + def run(self): + """Process all pages in generator.""" + if not hasattr(self, 'generator'): + raise NotImplementedError('Variable %s.generator not set.' + % self.__class__.__name__) + try: + for page in self.generator: + self.treat(page) + except QuitKeyboardInterrupt: + pywikibot.output('\nUser quit %s bot run...' % + self.__class__.__name__) + except KeyboardInterrupt: + # TODO: If the ^C occurred during an input() + # it should be handled as a QuitKeyboardInterrupt + # as developers shouldnt need a backtrace to find + # where the input() code is. + if config.verbose_output: + raise + else: + pywikibot.output('\nKeyboardInterrupt during %s bot run...' % + self.__class__.__name__)
class WikidataBot: diff --git a/scripts/add_text.py b/scripts/add_text.py index acbf321..00c9837 100644 --- a/scripts/add_text.py +++ b/scripts/add_text.py @@ -118,6 +118,11 @@ def add_text(page, addText, summary=None, regexSkip=None, regexSkipUrl=None, always=False, up=False, putText=True, oldTextGiven=None, reorderEnabled=True, create=False): + """ + Add text to a page. + + @rtype: tuple of (text, newtext, always) + """ site = page.site if not summary: summary = i18n.twtranslate(site, 'add_text-adding', diff --git a/scripts/capitalize_redirects.py b/scripts/capitalize_redirects.py index 35906ff..4db3e40 100644 --- a/scripts/capitalize_redirects.py +++ b/scripts/capitalize_redirects.py @@ -47,16 +47,10 @@
super(CapitalizeBot, self).__init__(**kwargs) self.generator = generator - self.done = False - - def run(self): - for page in self.generator: - if self.done: - break - if page.exists(): - self.treat(page)
def treat(self, page): + if not page.exists(): + return if page.isRedirectPage(): page = page.getRedirectTarget() page_t = page.title() @@ -81,7 +75,7 @@ if choice == 'a': self.options['always'] = True elif choice == 'q': - self.done = True + self.quit() if self.getOption('always') or choice == 'y': comment = i18n.twtranslate( page.site, @@ -114,10 +108,7 @@ if gen: preloadingGen = pagegenerators.PreloadingGenerator(gen) bot = CapitalizeBot(preloadingGen, **options) - try: - bot.run() - except KeyboardInterrupt: - pywikibot.output('\nQuitting program...') + bot.run() else: pywikibot.showHelp()
diff --git a/scripts/catall.py b/scripts/catall.py index 780e52d..ccd9f5c 100755 --- a/scripts/catall.py +++ b/scripts/catall.py @@ -21,7 +21,6 @@ __version__ = '$Id$' #
-import sys import pywikibot from pywikibot import i18n, textlib
@@ -55,8 +54,7 @@ chosen = None done = True elif choice == "q": - print "quit..." - sys.exit() + raise pywikibot.QuitKeyboardInterrupt else: chosen.append(choice) return chosen @@ -113,4 +111,7 @@ pywikibot.output(u'%s is a redirect' % p.title())
if __name__ == "__main__": - main() + try: + main() + except KeyboardInterrupt: + pywikibot.output('\nQuitting program...') diff --git a/scripts/clean_sandbox.py b/scripts/clean_sandbox.py index dbd1e24..052ec14 100755 --- a/scripts/clean_sandbox.py +++ b/scripts/clean_sandbox.py @@ -53,7 +53,7 @@ import datetime import sys import pywikibot -from pywikibot import i18n +from pywikibot import i18n, Bot
content = { 'commons': u'{{Sandbox}}\n<!-- Please edit only below this line. -->', @@ -136,7 +136,7 @@ }
-class SandboxBot(pywikibot.Bot): +class SandboxBot(Bot): availableOptions = { 'hours': 1, 'no_repeat': True, @@ -304,10 +304,7 @@ return
bot = SandboxBot(**opts) - try: - bot.run() - except KeyboardInterrupt: - pywikibot.output('\nQuitting program...') + bot.run()
if __name__ == "__main__": main() diff --git a/scripts/commonscat.py b/scripts/commonscat.py index 082cb9b..6a28ffc 100755 --- a/scripts/commonscat.py +++ b/scripts/commonscat.py @@ -65,9 +65,9 @@
import re
-import add_text +from add_text import add_text import pywikibot -from pywikibot import i18n, pagegenerators +from pywikibot import i18n, pagegenerators, Bot
docuReplacements = { '¶ms;': pagegenerators.parameterHelp @@ -234,17 +234,13 @@ }
-class CommonscatBot: +class CommonscatBot(Bot):
def __init__(self, generator, always, summary=None): + super(CommonscatBot, self).__init__(always=always) self.generator = generator - self.always = always self.summary = summary self.site = pywikibot.Site() - - def run(self): - for page in self.generator: - self.treat(page)
def treat(self, page): """ Load the given page, do some changes, and save it. """ @@ -262,46 +258,6 @@ % page.title(asLink=True)) else: self.addCommonscat(page) - - def save(self, text, page, comment, minorEdit=True, botflag=True): - # only save if something was changed - if text != page.get(): - # Show the title of the page we're working on. - # Highlight the title in purple. - pywikibot.output(u"\n\n>>> \03{lightpurple}%s\03{default} <<<" - % page.title()) - # show what was changed - pywikibot.showDiff(page.get(), text) - pywikibot.output(u'Comment: %s' % comment) - if not self.always: - choice = pywikibot.inputChoice( - u'Do you want to accept these changes?', - ['Yes', 'No', 'Always', 'Quit'], - ['y', 'N', 'a', 'q'], 'N') - if choice == 'a': - self.always = True - elif choice == 'q': - import sys - sys.exit() - if self.always or choice == 'y': - try: - # Save the page - page.put(text, comment=comment, - minorEdit=minorEdit, botflag=botflag) - except pywikibot.LockedPage: - pywikibot.output(u"Page %s is locked; skipping." - % page.title(asLink=True)) - except pywikibot.EditConflict: - pywikibot.output( - u'Skipping %s because of edit conflict' - % (page.title())) - except pywikibot.SpamfilterError as error: - pywikibot.output( - u'Cannot change %s because of spam blacklist entry %s' - % (page.title(), error.url)) - else: - return True - return False
@classmethod def getCommonscatTemplate(self, code=None): @@ -382,11 +338,9 @@ else: textToAdd = u'{{%s|%s}}' % (primaryCommonscat, commonscatLink) - (success, status, self.always) = add_text.add_text(page, - textToAdd, - self.summary, - None, None, - self.always) + always = self.getOption('always') + rv = add_text(page, textToAdd, self.summary, always=always) + self.options['always'] = rv[2] return True return True
@@ -423,7 +377,20 @@ msg_change, fallback=True) % {'oldcat': oldcat, 'newcat': newcat} - self.save(newtext, page, comment) + + try: + self.userPut(page, page.text, newtext, comment=comment) + except pywikibot.LockedPage: + pywikibot.output(u"Page %s is locked; skipping." + % page.title(asLink=True)) + except pywikibot.EditConflict: + pywikibot.output( + u'Skipping %s because of edit conflict' + % (page.title())) + except pywikibot.SpamfilterError as error: + pywikibot.output( + u'Cannot change %s because of spam blacklist entry %s' + % (page.title(), error.url))
def findCommonscatLink(self, page=None): # In Pywikibot 2.0, page.interwiki() now returns Link objects, not Page objects diff --git a/scripts/cosmetic_changes.py b/scripts/cosmetic_changes.py index aad7bf8..aaf3b52 100755 --- a/scripts/cosmetic_changes.py +++ b/scripts/cosmetic_changes.py @@ -848,7 +848,6 @@ super(CosmeticChangesBot, self).__init__(**kwargs)
self.generator = generator - self.done = False
def treat(self, page): try: @@ -873,15 +872,6 @@ except pywikibot.EditConflict: pywikibot.output("An edit conflict has occured at %s." % page.title(asLink=True)) - - def run(self): - try: - for page in self.generator: - if self.done: - break - self.treat(page) - except KeyboardInterrupt: - pywikibot.output('\nQuitting program...')
def main(): diff --git a/scripts/movepages.py b/scripts/movepages.py index e5329a1..618ac26 100644 --- a/scripts/movepages.py +++ b/scripts/movepages.py @@ -41,7 +41,6 @@ __version__ = '$Id$' #
-import sys import re import pywikibot from pywikibot import i18n, pagegenerators, Bot @@ -121,7 +120,7 @@ self.options['always'] = True self.moveOne(page, newPageTitle) elif choice2 == 'q': - sys.exit() + self.quit() elif choice2 == 'n': pass else: @@ -161,7 +160,7 @@ self.appendAll = True self.moveOne(page, newPageTitle) elif choice2 == 'q': - sys.exit() + self.quit() elif choice2 == 'n': pass else: @@ -195,7 +194,7 @@ self.regexAll = True self.moveOne(page, newPageTitle) elif choice2 == 'q': - sys.exit() + self.quit() elif choice2 == 'n': pass else: @@ -203,13 +202,9 @@ elif choice == 'n': pass elif choice == 'q': - sys.exit() + self.quit() else: self.treat(page) - - def run(self): - for page in self.generator: - self.treat(page)
def main(): diff --git a/scripts/redirect.py b/scripts/redirect.py index 454d1f3..72e64bb 100755 --- a/scripts/redirect.py +++ b/scripts/redirect.py @@ -379,20 +379,6 @@ self.exiting = False self._valid_template = None
- def prompt(self, question): - if not self.getOption('always'): - choice = pywikibot.inputChoice(question, - ['Yes', 'No', 'All', 'Quit'], - ['y', 'N', 'a', 'q'], 'N') - if choice == 'n': - return False - elif choice == 'q': - self.exiting = True - return False - elif choice == 'a': - self.options['always'] = True - return True - def has_valid_template(self, twtitle): """Check whether a template from translatewiki.net does exist on real wiki. We assume we are always working on self.site @@ -415,8 +401,6 @@ # get reason for deletion text for redir_name in self.generator.retrieve_broken_redirects(): self.delete_1_broken_redirect(redir_name) - if self.exiting: - break
def moved_page(self, source): gen = iter(self.site.logevents(logtype='move', page=source, total=1)) @@ -471,7 +455,7 @@ content) pywikibot.showDiff(content, text) pywikibot.output(u'Summary - %s' % reason) - if self.prompt( + if self.user_confirm( u'Redirect target %s has been moved to %s.\n' u'Do you want to fix %s?' % (targetPage, movedTarget, redir_page)): @@ -487,7 +471,7 @@ pywikibot.output(u'%s is locked.' % redir_page.title()) pass - elif self.getOption('delete') and self.prompt( + elif self.getOption('delete') and self.user_confirm( u'Redirect target %s does not exist.\n' u'Do you want to delete %s?' % (targetPage.title(asLink=True), @@ -532,8 +516,6 @@ def fix_double_redirects(self): for redir_name in self.generator.retrieve_double_redirects(): self.fix_1_double_redirect(redir_name) - if self.exiting: - break
def fix_1_double_redirect(self, redir_name): redir = pywikibot.Page(self.site, redir_name) @@ -677,7 +659,7 @@ {'to': targetPage.title(asLink=True)} ) pywikibot.showDiff(oldText, text) - if self.prompt(u'Do you want to accept the changes?'): + if self.user_confirm(u'Do you want to accept the changes?'): try: redir.put(text, summary) except pywikibot.LockedPage: @@ -713,10 +695,11 @@ else: self.fix_1_double_redirect(redir_name) count += 1 - if self.exiting or (self.getOption('number') and count >= self.getOption('number')): + if self.getOption('number') and count >= self.getOption('number'): break
def run(self): + """Run the script method selected by 'action' parameter.""" # TODO: make all generators return a redirect type indicator, # thus make them usable with 'both' if self.action == 'double': diff --git a/scripts/replace.py b/scripts/replace.py index c88dcdc..adb31ce 100755 --- a/scripts/replace.py +++ b/scripts/replace.py @@ -126,9 +126,8 @@ import re import time import pywikibot -from pywikibot import pagegenerators +from pywikibot import i18n, textlib, pagegenerators, Bot from pywikibot import editor as editarticle -from pywikibot import i18n, textlib import webbrowser
# Imports predefined replacements tasks from fixes.py @@ -219,7 +218,7 @@ return False
-class ReplaceRobot: +class ReplaceRobot(Bot): """ A bot that can do text replacements. """ @@ -399,7 +398,7 @@ new_text = original_text continue if choice == 'q': - return + self.quit() if choice == 'a': self.acceptall = True if choice == 'y': diff --git a/scripts/solve_disambiguation.py b/scripts/solve_disambiguation.py index 92d9d53..1d15922 100644 --- a/scripts/solve_disambiguation.py +++ b/scripts/solve_disambiguation.py @@ -79,14 +79,12 @@ #
import re -import sys import codecs
import pywikibot from pywikibot import editor as editarticle -from pywikibot import pagegenerators -from pywikibot import config -from pywikibot import i18n +from pywikibot import pagegenerators, config, i18n +from pywikibot.bot import Bot, QuitKeyboardInterrupt
# Disambiguation Needed template dn_template = { @@ -437,7 +435,7 @@ pass
-class DisambiguationRobot(object): +class DisambiguationRobot(Bot):
ignore_contents = { 'de': (u'{{[Ii]nuse}}', @@ -714,7 +712,7 @@ return True elif choice in ['q', 'Q']: # quit the program - return False + self.quit() elif choice in ['s', 'S']: # Next link on this page n -= 1 @@ -876,7 +874,7 @@ Please enter the name of the page where the redirect should have pointed at, or press enter to quit:""") if user_input == "": - sys.exit(1) + self.quit() else: self.alternatives.append(user_input) except pywikibot.IsNotRedirectPage: @@ -1001,9 +999,12 @@ preloadingGen = pagegenerators.PreloadingGenerator(gen) for refPage in preloadingGen: if not self.primaryIgnoreManager.isIgnored(refPage): - # run until the user selected 'quit' - if not self.treat(refPage, disambPage): - break + try: + self.treat(refPage, disambPage) + except QuitKeyboardInterrupt: + pywikibot.output('\nUser quit %s bot run...' % + self.__class__.__name__) + return
# clear alternatives before working on next disambiguation page self.alternatives = [] diff --git a/scripts/unlink.py b/scripts/unlink.py index b6b5d09..862cd36 100755 --- a/scripts/unlink.py +++ b/scripts/unlink.py @@ -68,7 +68,6 @@ # note that the definition of 'letter' varies from language to language. self.linkR = re.compile(r'[[(?P<title>[^]|#]*)(?P<section>#[^]|]*)?(|(?P<label>[^]]*))?]](?P<linktrail>%s)' % linktrail) - self.done = False self.comment = i18n.twtranslate(self.pageToUnlink.site, 'unlink-unlinking', self.pageToUnlink.title())
@@ -128,8 +127,7 @@ elif choice == 'a': self.options['always'] = True elif choice == 'q': - self.done = True - return text, False + self.quit() new = match.group('label') or match.group('title') new += match.group('linktrail') return text[:match.start()] + new + text[match.end():], False @@ -167,12 +165,6 @@ % page.title(asLink=True)) except pywikibot.LockedPage: pywikibot.output(u"Page %s is locked?!" % page.title(asLink=True)) - - def run(self): - for page in self.generator: - if self.done: - break - self.treat(page)
def main(): diff --git a/tests/script_tests.py b/tests/script_tests.py index 79e7f27..7e0100d 100644 --- a/tests/script_tests.py +++ b/tests/script_tests.py @@ -65,7 +65,7 @@ 'catall': 'q\n', # q for quit 'editarticle': 'Test page\n', 'interwiki': 'Test page\n', - # 'misspelling': 'q\n', # pressing 'q' doesnt work. bug 68663 + 'misspelling': 'q\n', 'pagefromfile': 'q\n', 'replace': 'foo\nbar\n\n\n', # match, replacement, # Enter to begin, Enter for default summary.