Source code for pywikibot.bot_choice

"""Choices for input_choice."""
# (C) Pywikibot team, 2015-2020
# Distributed under the terms of the MIT license.
import re

from textwrap import fill
from typing import Optional

import pywikibot

[docs]class Option: """ A basic option for input_choice. The following methods need to be implemented: * format(default=None) * result(value) * test(value) The methods C{test} and C{handled} are in such a relationship that when C{handled} returns itself that C{test} must return True for that value. So if C{test} returns False C{handled} may not return itself but it may return not None. Also C{result} only returns a sensible value when C{test} returns True for the same value. """
[docs] def __init__(self, stop=True) -> None: """Initializer.""" self._stop = stop
[docs] @staticmethod def formatted(text: str, options, default=None) -> str: """ Create a text with the options formatted into it. @param text: Text into which options are to be formatted @param options: Option instances to be formatted @type options: Iterable @return: Text with the options formatted into it """ formatted_options = [] for option in options: formatted_options.append(option.format(default=default)) # remove color highlights before fill function text = '{0} ({1})'.format(text, ', '.join(formatted_options)) pattern = '\03{[a-z]+}' highlights = re.findall(pattern, text) return fill(re.sub(pattern, '{}', text), width=77).format(*highlights)
@property def stop(self) -> bool: """Return whether this option stops asking.""" return self._stop
[docs] def handled(self, value): """ Return the Option object that applies to the given value. If this Option object doesn't know which applies it returns None. """ if self.test(value): return self else: return None
[docs] def format(self, default=None): """Return a formatted string for that option.""" raise NotImplementedError()
[docs] def result(self, value): """Return the actual value which is associated by the given one.""" raise NotImplementedError()
[docs] def test(self, value): """Return True whether this option applies.""" raise NotImplementedError()
[docs]class OutputOption(Option): """An option that never stops and can output on each question.""" before_question = False @property def stop(self): """Never stop asking.""" return False
[docs] def result(self, value): """Just output the value.""" self.output()
[docs] def output(self): """Output a string when selected and possibly before the question.""" raise NotImplementedError()
[docs]class StandardOption(Option): """An option with a description and shortcut and returning the shortcut."""
[docs] def __init__(self, option: str, shortcut: str, **kwargs): """ Initializer. @param option: option string @param shortcut: Shortcut of the option """ super().__init__(**kwargs) self.option = option self.shortcut = shortcut.lower()
[docs] def format(self, default=None) -> str: """Return a formatted string for that option.""" index = self.option.lower().find(self.shortcut) shortcut = self.shortcut if self.shortcut == default: shortcut = self.shortcut.upper() if index >= 0: return '{0}[{1}]{2}'.format( self.option[:index], shortcut, self.option[index + len(self.shortcut):]) else: return '{0} [{1}]'.format(self.option, shortcut)
[docs] def result(self, value): """Return the lowercased shortcut.""" return self.shortcut
[docs] def test(self, value) -> bool: """Return True whether this option applies.""" return (self.shortcut.lower() == value.lower() or self.option.lower() == value.lower())
[docs]class OutputProxyOption(OutputOption, StandardOption): """An option which calls output of the given output class."""
[docs] def __init__(self, option, shortcut, output, **kwargs): """Create a new option for the given sequence.""" super().__init__(option, shortcut, **kwargs) self._outputter = output
[docs] def output(self): """Output the contents.""" self._outputter.output()
[docs]class NestedOption(OutputOption, StandardOption): """ An option containing other options. It will return True in test if this option applies but False if a sub option applies while handle returns the sub option. """
[docs] def __init__(self, option, shortcut, description, options): """Initializer.""" super().__init__(option, shortcut, stop=False) self.description = description self.options = options
[docs] def format(self, default=None): """Return a formatted string for that option.""" self._output = Option.formatted(self.description, self.options) return super().format(default=default)
[docs] def handled(self, value): """Return itself if it applies or the applying sub option.""" for option in self.options: handled = option.handled(value) if handled is not None: return handled else: return super().handled(value)
[docs] def output(self): """Output the suboptions.""" pywikibot.output(self._output)
[docs]class ContextOption(OutputOption, StandardOption): """An option to show more and more context."""
[docs] def __init__( self, option, shortcut, text, context, delta=100, start=0, end=0 ): """Initializer.""" super().__init__(option, shortcut, stop=False) self.text = text self.context = context = delta self.start = start self.end = end
[docs] def result(self, value): """Add the delta to the context and output it.""" self.context += super().result(value)
[docs] def output(self): """Output the context.""" start = max(0, self.start - self.context) end = min(len(self.text), self.end + self.context) self.output_range(start, end)
[docs] def output_range(self, start_context, end_context): """Output a section from the text.""" pywikibot.output(self.text[start_context:end_context])
[docs]class Choice(StandardOption): """A simple choice consisting of an option, shortcut and handler."""
[docs] def __init__(self, option, shortcut, replacer): """Initializer.""" super().__init__(option, shortcut) self._replacer = replacer
@property def replacer(self): """The replacer.""" return self._replacer
[docs] def handle(self): """Handle this choice. Must be implemented.""" raise NotImplementedError()
[docs]class StaticChoice(Choice): """A static choice which just returns the given value."""
[docs] def __init__(self, option, shortcut, result): """Create instance with replacer set to None.""" super().__init__(option, shortcut, None) self._result = result
[docs] def handle(self): """Return the predefined value.""" return self._result
[docs]class LinkChoice(Choice): """A choice returning a mix of the link new and current link."""
[docs] def __init__(self, option, shortcut, replacer, replace_section, replace_label): """Initializer.""" super().__init__(option, shortcut, replacer) self._section = replace_section self._label = replace_label
[docs] def handle(self): """Handle by either applying the new section or label.""" kwargs = {} if self._section: kwargs['section'] = self.replacer._new.section else: kwargs['section'] = self.replacer.current_link.section if self._label: if self.replacer._new.anchor is None: kwargs['label'] = self.replacer._new.canonical_title() if self.replacer._new.section: kwargs['label'] += '#' + self.replacer._new.section else: kwargs['label'] = self.replacer._new.anchor else: if self.replacer.current_link.anchor is None: kwargs['label'] = self.replacer.current_groups['title'] if self.replacer.current_groups['section']: kwargs['label'] += '#' + \ self.replacer.current_groups['section'] else: kwargs['label'] = self.replacer.current_link.anchor return pywikibot.Link.create_separated( self.replacer._new.canonical_title(),, **kwargs)
[docs]class AlwaysChoice(Choice): """Add an option to always apply the default."""
[docs] def __init__(self, replacer, option='always', shortcut='a'): """Initializer.""" super().__init__(option, shortcut, replacer) self.always = False
[docs] def handle(self): """Handle the custom shortcut.""" self.always = True return self.answer
@property def answer(self): """Get the actual default answer instructing the replacement.""" return self.replacer.handle_answer(self.replacer._default)
[docs]class IntegerOption(Option): """An option allowing a range of integers."""
[docs] def __init__(self, minimum=1, maximum=None, prefix='', **kwargs): """Initializer.""" super().__init__(**kwargs) if not ((minimum is None or isinstance(minimum, int)) and (maximum is None or isinstance(maximum, int))): raise ValueError( 'The minimum and maximum parameters must be int or None.') if minimum is not None and maximum is not None and minimum > maximum: raise ValueError('The minimum must be lower than the maximum.') self._min = minimum self._max = maximum self.prefix = prefix
[docs] def test(self, value) -> bool: """Return whether the value is an int and in the specified range.""" try: value = self.parse(value) except ValueError: return False else: return ((self.minimum is None or value >= self.minimum) and (self.maximum is None or value <= self.maximum))
@property def minimum(self): """Return the lower bound of the range of allowed values.""" return self._min @property def maximum(self): """Return the upper bound of the range of allowed values.""" return self._max
[docs] def format(self, default=None) -> str: """Return a formatted string showing the range.""" if default is not None and self.test(default): value = self.parse(default) default = '[{0}]'.format(value) else: value = None default = '' if self.minimum is not None or self.maximum is not None: if default and value == self.minimum: minimum = default default = '' else: minimum = '' if self.minimum is None else str(self.minimum) if default and value == self.maximum: maximum = default default = '' else: maximum = '' if self.maximum is None else str(self.maximum) default = '-{0}-'.format(default) if default else '-' if self.minimum == self.maximum: rng = minimum else: rng = minimum + default + maximum else: rng = 'any' + default return '{0}<number> [{1}]'.format(self.prefix, rng)
[docs] def parse(self, value) -> int: """Return integer from value with prefix removed.""" if value.lower().startswith(self.prefix.lower()): return int(value[len(self.prefix):]) else: raise ValueError('Value does not start with prefix')
[docs] def result(self, value): """Return the value converted into int.""" return (self.prefix, self.parse(value))
[docs]class ListOption(IntegerOption): """An option to select something from a list."""
[docs] def __init__(self, sequence, prefix='', **kwargs): """Initializer.""" self._list = sequence try: super().__init__(1, self.maximum, prefix, **kwargs) except ValueError: raise ValueError('The sequence is empty.') del self._max
[docs] def format(self, default=None): """Return a string showing the range.""" if not self._list: raise ValueError('The sequence is empty.') else: return super().format(default=default)
@property def maximum(self) -> int: """Return the maximum value.""" return len(self._list)
[docs] def result(self, value): """Return a tuple with the prefix and selected value.""" return (self.prefix, self._list[self.parse(value) - 1])
[docs]class ShowingListOption(ListOption, OutputOption): """An option to show a list and select an item.""" before_question = True
[docs] def __init__(self, sequence, prefix='', pre: Optional[str] = None, post: Optional[str] = None, **kwargs): """Initializer. @param pre: Additional comment printed before the list. @param post: Additional comment printed after the list. """ super().__init__(sequence, prefix, **kwargs) self.pre = pre = post
@property def stop(self): """Return whether this option stops asking.""" return self._stop
[docs] def output(self): """Output the enumerated list.""" if self.pre is not None: pywikibot.output(self.pre) width = len(str(self.maximum)) for i, item in enumerate(self._list, self.minimum): pywikibot.output('{:>{width}} - {}'.format(i, item, width=width)) if is not None: pywikibot.output(
[docs]class MultipleChoiceList(ListOption): """An option to select multiple items from a list."""
[docs] def test(self, value) -> bool: """Return whether the values are int and in the specified range.""" try: values = [self.parse(val) for val in value.split(',')] except ValueError: return False for val in values: if self.minimum is not None and val < self.minimum: break if self.maximum is not None and val > self.maximum: break else: return True return False
[docs] def result(self, value): """Return a tuple with the prefix and selected values as a list.""" values = (self.parse(val) for val in value.split(',')) result = [self._list[val - 1] for val in values] return (self.prefix, result)
[docs]class ShowingMultipleChoiceList(ShowingListOption, MultipleChoiceList): """An option to show a list and select multiple items.""" pass
[docs]class HighlightContextOption(ContextOption): """Show the original region highlighted."""
[docs] def output_range(self, start, end): """Show normal context with a red center region.""" pywikibot.output(self.text[start:self.start] + '\03{lightred}' + self.text[self.start:self.end] + '\03{default}' + self.text[self.end:end])
[docs]class ChoiceException(StandardOption, Exception): """A choice for input_choice which result in this exception."""
[docs] def result(self, value): """Return itself to raise the exception.""" return self
[docs]class QuitKeyboardInterrupt(ChoiceException, KeyboardInterrupt): """The user has cancelled processing at a prompt."""
[docs] def __init__(self): """Constructor using the 'quit' ('q') in input_choice.""" super().__init__('quit', 'q')