mirror of
https://github.com/certbot/certbot.git
synced 2026-07-30 18:04:31 +02:00
Noninteractive iDisplay basic implementation
(no tests or hooks, yet)
This commit is contained in:
+49
-195
@@ -6,7 +6,7 @@ import dialog
|
|||||||
import zope.interface
|
import zope.interface
|
||||||
|
|
||||||
from letsencrypt import interfaces
|
from letsencrypt import interfaces
|
||||||
|
from letsencrypt import errors
|
||||||
|
|
||||||
WIDTH = 72
|
WIDTH = 72
|
||||||
HEIGHT = 20
|
HEIGHT = 20
|
||||||
@@ -21,6 +21,20 @@ CANCEL = "cancel"
|
|||||||
HELP = "help"
|
HELP = "help"
|
||||||
"""Display exit code when for when the user requests more help."""
|
"""Display exit code when for when the user requests more help."""
|
||||||
|
|
||||||
|
def _wrap_lines(msg): # pylint: disable=no-self-use
|
||||||
|
"""Format lines nicely to 80 chars.
|
||||||
|
|
||||||
|
:param str msg: Original message
|
||||||
|
|
||||||
|
:returns: Formatted message respecting newlines in message
|
||||||
|
:rtype: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
lines = msg.splitlines()
|
||||||
|
fixed_l = []
|
||||||
|
for line in lines:
|
||||||
|
fixed_l.append(textwrap.fill(line, 80))
|
||||||
|
return os.linesep.join(fixed_l)
|
||||||
|
|
||||||
class NcursesDisplay(object):
|
class NcursesDisplay(object):
|
||||||
"""Ncurses-based display."""
|
"""Ncurses-based display."""
|
||||||
@@ -178,7 +192,7 @@ class FileDisplay(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
side_frame = "-" * 79
|
side_frame = "-" * 79
|
||||||
message = self._wrap_lines(message)
|
message = _wrap_lines(message)
|
||||||
self.outfile.write(
|
self.outfile.write(
|
||||||
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
|
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
|
||||||
line=os.linesep, frame=side_frame, msg=message))
|
line=os.linesep, frame=side_frame, msg=message))
|
||||||
@@ -246,7 +260,7 @@ class FileDisplay(object):
|
|||||||
"""
|
"""
|
||||||
side_frame = ("-" * 79) + os.linesep
|
side_frame = ("-" * 79) + os.linesep
|
||||||
|
|
||||||
message = self._wrap_lines(message)
|
message = _wrap_lines(message)
|
||||||
|
|
||||||
self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
|
self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
|
||||||
os.linesep, frame=side_frame, msg=message))
|
os.linesep, frame=side_frame, msg=message))
|
||||||
@@ -352,21 +366,6 @@ class FileDisplay(object):
|
|||||||
|
|
||||||
self.outfile.write(side_frame)
|
self.outfile.write(side_frame)
|
||||||
|
|
||||||
def _wrap_lines(self, msg): # pylint: disable=no-self-use
|
|
||||||
"""Format lines nicely to 80 chars.
|
|
||||||
|
|
||||||
:param str msg: Original message
|
|
||||||
|
|
||||||
:returns: Formatted message respecting newlines in message
|
|
||||||
:rtype: str
|
|
||||||
|
|
||||||
"""
|
|
||||||
lines = msg.splitlines()
|
|
||||||
fixed_l = []
|
|
||||||
for line in lines:
|
|
||||||
fixed_l.append(textwrap.fill(line, 80))
|
|
||||||
|
|
||||||
return os.linesep.join(fixed_l)
|
|
||||||
|
|
||||||
def _get_valid_int_ans(self, max_):
|
def _get_valid_int_ans(self, max_):
|
||||||
"""Get a numerical selection.
|
"""Get a numerical selection.
|
||||||
@@ -409,20 +408,22 @@ class NoninteractiveDisplay(object):
|
|||||||
zope.interface.implements(interfaces.IDisplay)
|
zope.interface.implements(interfaces.IDisplay)
|
||||||
|
|
||||||
def __init__(self, outfile):
|
def __init__(self, outfile):
|
||||||
super(FileDisplay, self).__init__()
|
super(NoninteractiveDisplay, self).__init__()
|
||||||
self.outfile = outfile
|
self.outfile = outfile
|
||||||
|
|
||||||
def _interaction_fail(self, message, extra=""):
|
def _interaction_fail(self, message, cli_flag, extra=""):
|
||||||
"Error out in case of an attempt to interact in noninteractive mode"
|
"Error out in case of an attempt to interact in noninteractive mode"
|
||||||
msg ="Missing command line flag or config entry for this setting:\n"
|
msg = "Missing command line flag or config entry for this setting:\n"
|
||||||
msg += message
|
msg += message
|
||||||
if extra:
|
if extra:
|
||||||
msg += "\n" + extra
|
msg += "\n" + extra
|
||||||
raise MissingCommandlineFlag, msg
|
if cli_flag:
|
||||||
|
msg += "\n\n(You can set this with the {0} flag)".format(cli_flag)
|
||||||
|
raise errors.MissingCommandlineFlag, msg
|
||||||
|
|
||||||
def notification(self, message, height=10, pause=True):
|
def notification(self, message, height=10, pause=False):
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
"""Displays a notification and waits for user acceptance.
|
"""Displays a notification without waiting for user acceptance.
|
||||||
|
|
||||||
:param str message: Message to display to stdout
|
:param str message: Message to display to stdout
|
||||||
:param int height: No effect for NoninteractiveDisplay
|
:param int height: No effect for NoninteractiveDisplay
|
||||||
@@ -430,23 +431,20 @@ class NoninteractiveDisplay(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
side_frame = "-" * 79
|
side_frame = "-" * 79
|
||||||
message = self._wrap_lines(message)
|
message = _wrap_lines(message)
|
||||||
self.outfile.write(
|
self.outfile.write(
|
||||||
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
|
"{line}{frame}{line}{msg}{line}{frame}{line}".format(
|
||||||
line=os.linesep, frame=side_frame, msg=message))
|
line=os.linesep, frame=side_frame, msg=message))
|
||||||
|
|
||||||
def menu(self, message, choices, ok_label="", cancel_label="",
|
def menu(self, message, choices, default=None, cli_flag=None, **kwargs):
|
||||||
help_label="", default=None):
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
"""Display a menu.
|
"""Avoid displaying a menu.
|
||||||
|
|
||||||
.. todo:: This doesn't enable the help label/button (I wasn't sold on
|
|
||||||
any interface I came up with for this). It would be a nice feature
|
|
||||||
|
|
||||||
:param str message: title of menu
|
:param str message: title of menu
|
||||||
:param choices: Menu lines, len must be > 0
|
:param choices: Menu lines, len must be > 0
|
||||||
:type choices: list of tuples (tag, item) or
|
:type choices: list of tuples (tag, item) or
|
||||||
list of descriptions (tags will be enumerated)
|
list of descriptions (tags will be enumerated)
|
||||||
|
:param dict kwargs: absorbs various irrelevant labelling arguments
|
||||||
|
|
||||||
:returns: tuple of the form (code, tag) where
|
:returns: tuple of the form (code, tag) where
|
||||||
code - int display exit code
|
code - int display exit code
|
||||||
@@ -456,17 +454,11 @@ class NoninteractiveDisplay(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if default is None:
|
if default is None:
|
||||||
self._interaction_fail(message, "Choices: " + repr(choices))
|
self._interaction_fail(message, cli_flag, "Choices: " + repr(choices))
|
||||||
if default == None:
|
|
||||||
msg ="Missing command line flag or config entry for this choice:\n"
|
|
||||||
msg += message
|
|
||||||
msg += "\nChoices: " + repr(choices)
|
|
||||||
raise MissingCommandlineFlag, msg
|
|
||||||
|
|
||||||
return OK, choices.index(default)
|
return OK, choices.index(default)
|
||||||
|
|
||||||
def input(self, message, default=None):
|
def input(self, message, default=None, cli_flag=None):
|
||||||
# pylint: disable=no-self-use
|
|
||||||
"""Accept input from the user.
|
"""Accept input from the user.
|
||||||
|
|
||||||
:param str message: message to display to the user
|
:param str message: message to display to the user
|
||||||
@@ -475,58 +467,39 @@ class NoninteractiveDisplay(object):
|
|||||||
`code` - str display exit code
|
`code` - str display exit code
|
||||||
`input` - str of the user's input
|
`input` - str of the user's input
|
||||||
:rtype: tuple
|
:rtype: tuple
|
||||||
|
:raises errors.MissingCommandlineFlag: if there was no default
|
||||||
|
|
||||||
"""
|
"""
|
||||||
ans = raw_input(
|
if default is None:
|
||||||
textwrap.fill("%s (Enter 'c' to cancel): " % message, 80))
|
self._interaction_fail(message, cli_flag)
|
||||||
|
|
||||||
if ans == "c" or ans == "C":
|
|
||||||
return CANCEL, "-1"
|
|
||||||
else:
|
else:
|
||||||
return OK, ans
|
return OK, default
|
||||||
|
|
||||||
def yesno(self, message, yes_label="Yes", no_label="No"):
|
|
||||||
"""Query the user with a yes/no question.
|
|
||||||
|
|
||||||
Yes and No label must begin with different letters, and must contain at
|
def yesno(self, message, default=None, cli_flag=None, **kwargs):
|
||||||
least one letter each.
|
# pylint: disable=unused-argument
|
||||||
|
"""Decide Yes or No, without asking anybody
|
||||||
|
|
||||||
:param str message: question for the user
|
:param str message: question for the user
|
||||||
:param str yes_label: Label of the "Yes" parameter
|
:param dict kwargs: absorbs yes_label, no_label
|
||||||
:param str no_label: Label of the "No" parameter
|
|
||||||
|
|
||||||
|
:raises errors.MissingCommandlineFlag: if there was no default
|
||||||
:returns: True for "Yes", False for "No"
|
:returns: True for "Yes", False for "No"
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
side_frame = ("-" * 79) + os.linesep
|
if default is None:
|
||||||
|
self._interaction_fail(message, cli_flag)
|
||||||
|
else:
|
||||||
|
return OK, default
|
||||||
|
|
||||||
message = self._wrap_lines(message)
|
def checklist(self, message, tags, default=None, cli_flag=None, **kwargs):
|
||||||
|
|
||||||
self.outfile.write("{0}{frame}{msg}{0}{frame}".format(
|
|
||||||
os.linesep, frame=side_frame, msg=message))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
ans = raw_input("{yes}/{no}: ".format(
|
|
||||||
yes=_parens_around_char(yes_label),
|
|
||||||
no=_parens_around_char(no_label)))
|
|
||||||
|
|
||||||
# Couldn't get pylint indentation right with elif
|
|
||||||
# elif doesn't matter in this situation
|
|
||||||
if (ans.startswith(yes_label[0].lower()) or
|
|
||||||
ans.startswith(yes_label[0].upper())):
|
|
||||||
return True
|
|
||||||
if (ans.startswith(no_label[0].lower()) or
|
|
||||||
ans.startswith(no_label[0].upper())):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def checklist(self, message, tags, default_status=True):
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
"""Display a checklist.
|
"""Display a checklist.
|
||||||
|
|
||||||
:param str message: Message to display to user
|
:param str message: Message to display to user
|
||||||
:param list tags: `str` tags to select, len(tags) > 0
|
:param list tags: `str` tags to select, len(tags) > 0
|
||||||
:param bool default_status: Not used for FileDisplay
|
:param dict kwargs: absorbs default_status arg
|
||||||
|
|
||||||
:returns: tuple of (`code`, `tags`) where
|
:returns: tuple of (`code`, `tags`) where
|
||||||
`code` - str display exit code
|
`code` - str display exit code
|
||||||
@@ -534,129 +507,10 @@ class NoninteractiveDisplay(object):
|
|||||||
:rtype: tuple
|
:rtype: tuple
|
||||||
|
|
||||||
"""
|
"""
|
||||||
while True:
|
if default is None:
|
||||||
self._print_menu(message, tags)
|
self._interaction_fail(message, cli_flag, "? ".join(tags))
|
||||||
|
|
||||||
code, ans = self.input("Select the appropriate numbers separated "
|
|
||||||
"by commas and/or spaces")
|
|
||||||
|
|
||||||
if code == OK:
|
|
||||||
indices = separate_list_input(ans)
|
|
||||||
selected_tags = self._scrub_checklist_input(indices, tags)
|
|
||||||
if selected_tags:
|
|
||||||
return code, selected_tags
|
|
||||||
else:
|
|
||||||
self.outfile.write(
|
|
||||||
"** Error - Invalid selection **%s" % os.linesep)
|
|
||||||
else:
|
|
||||||
return code, []
|
|
||||||
|
|
||||||
def _scrub_checklist_input(self, indices, tags):
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
"""Validate input and transform indices to appropriate tags.
|
|
||||||
|
|
||||||
:param list indices: input
|
|
||||||
:param list tags: Original tags of the checklist
|
|
||||||
|
|
||||||
:returns: valid tags the user selected
|
|
||||||
:rtype: :class:`list` of :class:`str`
|
|
||||||
|
|
||||||
"""
|
|
||||||
# They should all be of type int
|
|
||||||
try:
|
|
||||||
indices = [int(index) for index in indices]
|
|
||||||
except ValueError:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Remove duplicates
|
|
||||||
indices = list(set(indices))
|
|
||||||
|
|
||||||
# Check all input is within range
|
|
||||||
for index in indices:
|
|
||||||
if index < 1 or index > len(tags):
|
|
||||||
return []
|
|
||||||
# Transform indices to appropriate tags
|
|
||||||
return [tags[index - 1] for index in indices]
|
|
||||||
|
|
||||||
def _print_menu(self, message, choices):
|
|
||||||
"""Print a menu on the screen.
|
|
||||||
|
|
||||||
:param str message: title of menu
|
|
||||||
:param choices: Menu lines
|
|
||||||
:type choices: list of tuples (tag, item) or
|
|
||||||
list of descriptions (tags will be enumerated)
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Can take either tuples or single items in choices list
|
|
||||||
if choices and isinstance(choices[0], tuple):
|
|
||||||
choices = ["%s - %s" % (c[0], c[1]) for c in choices]
|
|
||||||
|
|
||||||
# Write out the message to the user
|
|
||||||
self.outfile.write(
|
|
||||||
"{new}{msg}{new}".format(new=os.linesep, msg=message))
|
|
||||||
side_frame = ("-" * 79) + os.linesep
|
|
||||||
self.outfile.write(side_frame)
|
|
||||||
|
|
||||||
# Write out the menu choices
|
|
||||||
for i, desc in enumerate(choices, 1):
|
|
||||||
self.outfile.write(
|
|
||||||
textwrap.fill("{num}: {desc}".format(num=i, desc=desc), 80))
|
|
||||||
|
|
||||||
# Keep this outside of the textwrap
|
|
||||||
self.outfile.write(os.linesep)
|
|
||||||
|
|
||||||
self.outfile.write(side_frame)
|
|
||||||
|
|
||||||
def _wrap_lines(self, msg): # pylint: disable=no-self-use
|
|
||||||
"""Format lines nicely to 80 chars.
|
|
||||||
|
|
||||||
:param str msg: Original message
|
|
||||||
|
|
||||||
:returns: Formatted message respecting newlines in message
|
|
||||||
:rtype: str
|
|
||||||
|
|
||||||
"""
|
|
||||||
lines = msg.splitlines()
|
|
||||||
fixed_l = []
|
|
||||||
for line in lines:
|
|
||||||
fixed_l.append(textwrap.fill(line, 80))
|
|
||||||
|
|
||||||
return os.linesep.join(fixed_l)
|
|
||||||
|
|
||||||
def _get_valid_int_ans(self, max_):
|
|
||||||
"""Get a numerical selection.
|
|
||||||
|
|
||||||
:param int max: The maximum entry (len of choices), must be positive
|
|
||||||
|
|
||||||
:returns: tuple of the form (`code`, `selection`) where
|
|
||||||
`code` - str display exit code ('ok' or cancel')
|
|
||||||
`selection` - int user's selection
|
|
||||||
:rtype: tuple
|
|
||||||
|
|
||||||
"""
|
|
||||||
selection = -1
|
|
||||||
if max_ > 1:
|
|
||||||
input_msg = ("Select the appropriate number "
|
|
||||||
"[1-{max_}] then [enter] (press 'c' to "
|
|
||||||
"cancel): ".format(max_=max_))
|
|
||||||
else:
|
else:
|
||||||
input_msg = ("Press 1 [enter] to confirm the selection "
|
return OK, default
|
||||||
"(press 'c' to cancel): ")
|
|
||||||
while selection < 1:
|
|
||||||
ans = raw_input(input_msg)
|
|
||||||
if ans.startswith("c") or ans.startswith("C"):
|
|
||||||
return CANCEL, -1
|
|
||||||
try:
|
|
||||||
selection = int(ans)
|
|
||||||
if selection < 1 or selection > max_:
|
|
||||||
selection = -1
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
self.outfile.write(
|
|
||||||
"{0}** Invalid input **{0}".format(os.linesep))
|
|
||||||
|
|
||||||
return OK, selection
|
|
||||||
|
|
||||||
|
|
||||||
def separate_list_input(input_):
|
def separate_list_input(input_):
|
||||||
|
|||||||
@@ -102,3 +102,8 @@ class StandaloneBindError(Error):
|
|||||||
|
|
||||||
class ConfigurationError(Error):
|
class ConfigurationError(Error):
|
||||||
"""Configuration sanity error."""
|
"""Configuration sanity error."""
|
||||||
|
|
||||||
|
# NoninteractiveDisplay iDisplay plugin error:
|
||||||
|
|
||||||
|
class MissingCommandlineFlag(Error):
|
||||||
|
"""A command line argument was missing in noninteractive usage"""
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ class FileOutputDisplayTest(unittest.TestCase):
|
|||||||
"This function is only meant to be for easy viewing{0}"
|
"This function is only meant to be for easy viewing{0}"
|
||||||
"Test a really really really really really really really really "
|
"Test a really really really really really really really really "
|
||||||
"really really really really long line...".format(os.linesep))
|
"really really really really long line...".format(os.linesep))
|
||||||
text = self.displayer._wrap_lines(msg)
|
text = display_util._wrap_lines(msg)
|
||||||
|
|
||||||
self.assertEqual(text.count(os.linesep), 3)
|
self.assertEqual(text.count(os.linesep), 3)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user