"""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
@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 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 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 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
self.delta = delta
self.start = start
self.end = end
[docs] def result(self, value):
"""Add the delta to the context and output it."""
self.context += self.delta
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] def handle_link(self):
"""The current link will be handled by this choice."""
return False
[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(), self.replacer._new.site,
**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
[docs] def handle_link(self):
"""Directly return answer whether it's applying it always."""
return self.always
@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 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
@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
self.post = 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 self.post is not None:
pywikibot.output(self.post)
[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')