From 432a85cd18f4b876434fd75d4a65f09222c7029a Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:19:57 -0700 Subject: [PATCH 01/12] Add Ncurses directory_select --- letsencrypt/display/util.py | 24 ++++++++++++++++++++++++ letsencrypt/tests/display/util_test.py | 5 +++++ 2 files changed, 29 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 84049c47c..3913f8bf1 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -11,6 +11,13 @@ from letsencrypt import errors WIDTH = 72 HEIGHT = 20 +DSELECT_HELP = ( + "Use the arrow keys or tab to move between window elements. Space can be " + "used to complete the input path with the selected element in the " + "directory window. Pressing enter will select the currently highlighted " + "button.") +"""Help text on how to use dialog's dselect.""" + # Display exit codes OK = "ok" """Display exit code indicating user acceptance.""" @@ -21,6 +28,7 @@ CANCEL = "cancel" HELP = "help" """Display exit code when for when the user requests more help.""" + def _wrap_lines(msg): """Format lines nicely to 80 chars. @@ -36,6 +44,7 @@ def _wrap_lines(msg): fixed_l.append(textwrap.fill(line, 80)) return os.linesep.join(fixed_l) + @zope.interface.implementer(interfaces.IDisplay) class NcursesDisplay(object): """Ncurses-based display.""" @@ -174,6 +183,21 @@ class NcursesDisplay(object): return self.dialog.checklist( message, width=self.width, height=self.height, choices=choices) + def directory_select(self, message, **unused_kwargs): + """Display a directory selection screen. + + :param str message: prompt to give the user + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + root_directory = os.path.abspath(os.sep) + return self.dialog.dselect( + filepath=root_directory, width=self.width, + height=self.height, help_button=True, title=message) + @zope.interface.implementer(interfaces.IDisplay) class FileDisplay(object): diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index a16eb544e..45025d7a0 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -123,6 +123,11 @@ class NcursesDisplayTest(unittest.TestCase): "message", width=display_util.WIDTH, height=display_util.HEIGHT, choices=choices) + @mock.patch("letsencrypt.display.util.dialog.Dialog.dselect") + def test_directory_select(self, mock_dselect): + self.displayer.directory_select("message") + self.assertEqual(mock_dselect.call_count, 1) + class FileOutputDisplayTest(unittest.TestCase): """Test stdout display. From 08232eef43680e189aaf2776f0953c2a22bd5dfe Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:22:15 -0700 Subject: [PATCH 02/12] display.util pep8 cleanup --- letsencrypt/display/util.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 3913f8bf1..05e280118 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -127,7 +127,6 @@ class NcursesDisplay(object): return code, int(index) - 1 - def input(self, message, **unused_kwargs): """Display an input box to the user. @@ -141,11 +140,10 @@ class NcursesDisplay(object): """ sections = message.split("\n") # each section takes at least one line, plus extras if it's longer than self.width - wordlines = [1 + (len(section)/self.width) for section in sections] + wordlines = [1 + (len(section) / self.width) for section in sections] height = 6 + sum(wordlines) + len(sections) return self.dialog.inputbox(message, width=self.width, height=height) - def yesno(self, message, yes_label="Yes", no_label="No", **unused_kwargs): """Display a Yes/No dialog box. @@ -397,7 +395,6 @@ class FileDisplay(object): self.outfile.write(side_frame) - def _get_valid_int_ans(self, max_): """Get a numerical selection. @@ -433,6 +430,7 @@ class FileDisplay(object): return OK, selection + @zope.interface.implementer(interfaces.IDisplay) class NoninteractiveDisplay(object): """An iDisplay implementation that never asks for interactive user input""" @@ -507,7 +505,6 @@ class NoninteractiveDisplay(object): else: return OK, default - def yesno(self, message, yes_label=None, no_label=None, default=None, cli_flag=None): # pylint: disable=unused-argument """Decide Yes or No, without asking anybody From 95a4a2ca6098f23322f7ed8e30c218d198e52903 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Mon, 21 Mar 2016 16:23:21 -0700 Subject: [PATCH 03/12] display.util_test cleanup --- letsencrypt/tests/display/util_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index 45025d7a0..adec265dc 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -285,6 +285,7 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer._get_valid_int_ans(3), (display_util.CANCEL, -1)) + class NoninteractiveDisplayTest(unittest.TestCase): """Test non-interactive display. From 74a7d2bed90a3ecadf73f723f913c6a6e4f0553b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Tue, 22 Mar 2016 18:07:51 -0700 Subject: [PATCH 04/12] Added completer.py and tests for FileDisplay --- letsencrypt/display/completer.py | 51 ++++++++++++++++ letsencrypt/tests/display/completer_test.py | 64 +++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 letsencrypt/display/completer.py create mode 100644 letsencrypt/tests/display/completer_test.py diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py new file mode 100644 index 000000000..5258ef52e --- /dev/null +++ b/letsencrypt/display/completer.py @@ -0,0 +1,51 @@ +"""Provides tab autocompletion when prompting users for a path.""" +import glob +import readline + + +class Completer(object): + """Provides tab autocompletion when prompting users for a path. + + This class is meant to be used with readline to provide tab + autocompletion for users entering paths. The complete method can + be passed to readline.set_completer directly, however, this function + works best as a context manager. For example: + + with Completer(): + raw_input() + + In this example, tab autocompletion will be available during + the call to raw_input above, however, readline will be restored to + its previous state when exiting the body of the with statement. + + """ + + def __init__(self): + self._completer = self._delims = self._iter = None + + def complete(self, text, state): + """Provides path autocompletion for use with readline. + + :param str text: text to offer completions for + :param int state: which completion to return + + :returns: possible completion for text or ``None`` if all + completions have been returned + :rtype: str + + """ + if state == 0: + self._iter = glob.iglob(text + '*') + return next(self._iter, None) + + def __enter__(self): + self._completer = readline.get_completer() + self._delims = readline.get_completer_delims() + + readline.set_completer(self.complete) + readline.set_completer_delims(' \t\n;') + readline.parse_and_bind('tab: complete') + + def __exit__(self, unused_type, unused_value, unused_traceback): + readline.set_completer_delims(self._delims) + readline.set_completer(self._completer) diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py new file mode 100644 index 000000000..dbba15106 --- /dev/null +++ b/letsencrypt/tests/display/completer_test.py @@ -0,0 +1,64 @@ +"""Test letsencrypt.display.completer.""" +import os +import readline +import shutil +import string +import tempfile +import unittest + + +class CompleterTest(unittest.TestCase): + """Test letsencrypt.display.completer.Completer.""" + + def setUp(self): + self.temp_dir = tempfile.mkdtemp() + + # directories must end with os.sep for completer to + # search inside the directory for possible completions + if self.temp_dir[-1] != os.sep: + self.temp_dir += os.sep + + self.paths = [] + # create some files and directories in temp_dir + for c in string.ascii_lowercase: + path = os.path.join(self.temp_dir, c) + self.paths.append(path) + if ord(c) % 2: + os.mkdir(path) + else: + with open(path, 'w'): + pass + + def tearDown(self): + shutil.rmtree(self.temp_dir) + + def test_context_manager(self): + from letsencrypt.display import completer + + original_completer = readline.get_completer() + original_delims = readline.get_completer_delims() + + with completer.Completer(): + pass + + self.assertEqual(readline.get_completer(), original_completer) + self.assertEqual(readline.get_completer_delims(), original_delims) + + def test_complete(self): + from letsencrypt.display import completer + + my_completer = completer.Completer() + num_paths = len(self.paths) + + for i in range(num_paths): + completion = my_completer.complete(self.temp_dir, i) + self.assertTrue(completion in self.paths) + self.paths.remove(completion) + + self.assertFalse(self.paths) + completion = my_completer.complete(self.temp_dir, num_paths) + self.assertEqual(completion, None) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover From 49fefb08dd126f881d33f8c32ec5caf78acf0e2c Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 10:35:38 -0700 Subject: [PATCH 05/12] autocomplete -> complete --- letsencrypt/display/completer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 5258ef52e..71e72b942 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,22 +1,22 @@ -"""Provides tab autocompletion when prompting users for a path.""" +"""Provides tab completion when prompting users for a path.""" import glob import readline class Completer(object): - """Provides tab autocompletion when prompting users for a path. + """Provides tab completion when prompting users for a path. This class is meant to be used with readline to provide tab - autocompletion for users entering paths. The complete method can - be passed to readline.set_completer directly, however, this function + completion for users entering paths. The complete method can be + passed to readline.set_completer directly, however, this function works best as a context manager. For example: with Completer(): raw_input() - In this example, tab autocompletion will be available during - the call to raw_input above, however, readline will be restored to - its previous state when exiting the body of the with statement. + In this example, tab completion will be available during the call to + raw_input above, however, readline will be restored to its previous + state when exiting the body of the with statement. """ @@ -24,7 +24,7 @@ class Completer(object): self._completer = self._delims = self._iter = None def complete(self, text, state): - """Provides path autocompletion for use with readline. + """Provides path completion for use with readline. :param str text: text to offer completions for :param int state: which completion to return From f9ac7d789b898353a79e74a07d93f71e63d362ee Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 11:15:37 -0700 Subject: [PATCH 06/12] Add support libedit readline --- letsencrypt/display/completer.py | 8 +++- letsencrypt/tests/display/completer_test.py | 52 ++++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 71e72b942..6b07a2e47 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -44,7 +44,13 @@ class Completer(object): readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') - readline.parse_and_bind('tab: complete') + + # readline can be implemented using GNU readline or libedit + # which have different configuration syntax + if 'libedit' in readline.__doc__: + readline.parse_and_bind('bind ^I rl_complete') + else: + readline.parse_and_bind('tab: complete') def __exit__(self, unused_type, unused_value, unused_traceback): readline.set_completer_delims(self._delims) diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py index dbba15106..a77d3c842 100644 --- a/letsencrypt/tests/display/completer_test.py +++ b/letsencrypt/tests/display/completer_test.py @@ -6,6 +6,8 @@ import string import tempfile import unittest +import mock + class CompleterTest(unittest.TestCase): """Test letsencrypt.display.completer.Completer.""" @@ -32,18 +34,6 @@ class CompleterTest(unittest.TestCase): def tearDown(self): shutil.rmtree(self.temp_dir) - def test_context_manager(self): - from letsencrypt.display import completer - - original_completer = readline.get_completer() - original_delims = readline.get_completer_delims() - - with completer.Completer(): - pass - - self.assertEqual(readline.get_completer(), original_completer) - self.assertEqual(readline.get_completer_delims(), original_delims) - def test_complete(self): from letsencrypt.display import completer @@ -59,6 +49,44 @@ class CompleterTest(unittest.TestCase): completion = my_completer.complete(self.temp_dir, num_paths) self.assertEqual(completion, None) + def test_context_manager(self): + from letsencrypt.display import completer + + original_completer = readline.get_completer() + original_delims = readline.get_completer_delims() + + with completer.Completer(): + pass + + self.assertEqual(readline.get_completer(), original_completer) + self.assertEqual(readline.get_completer_delims(), original_delims) + + @mock.patch('letsencrypt.display.completer.readline', autospec=True) + def test_context_manager_libedit(self, mock_readline): + mock_readline.__doc__ = 'libedit' + self._test_mocked_readline(mock_readline) + + @mock.patch('letsencrypt.display.completer.readline', autospec=True) + def test_context_manager_readline(self, mock_readline): + mock_readline.__doc__ = 'GNU readline' + self._test_mocked_readline(mock_readline) + + def _test_mocked_readline(self, mock_readline): + from letsencrypt.display import completer + + mock_readline.parse_and_bind.side_effect = enable_tab_completion + + with completer.Completer(): + pass + + self.assertTrue(mock_readline.parse_and_bind.called) + + +def enable_tab_completion(unused_command): + """Enables readline tab completion using the system specific syntax.""" + libedit = 'libedit' in readline.__doc__ + command = 'bind ^I rl_complete' if libedit else 'tab: complete' + readline.parse_and_bind(command) if __name__ == "__main__": unittest.main() # pragma: no cover From 0c0687ca68ae4b9c4fa2585bfd1a974d121bed30 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 11:27:20 -0700 Subject: [PATCH 07/12] Add dummy_readline module --- letsencrypt/display/dummy_readline.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 letsencrypt/display/dummy_readline.py diff --git a/letsencrypt/display/dummy_readline.py b/letsencrypt/display/dummy_readline.py new file mode 100644 index 000000000..fb3d807bb --- /dev/null +++ b/letsencrypt/display/dummy_readline.py @@ -0,0 +1,21 @@ +"""A dummy module with no effect for use on systems without readline.""" + + +def get_completer(): + """An empty implementation of readline.get_completer.""" + + +def get_completer_delims(): + """An empty implementation of readline.get_completer_delims.""" + + +def parse_and_bind(unused_command): + """An empty implementation of readline.parse_and_bind.""" + + +def set_completer(unused_function=None): + """An empty implementation of readline.set_completer.""" + + +def set_completer_delims(unused_delims): + """An empty implementation of readline.set_completer_delims.""" From 7820c687f16550c7a406209918092cfbbd9c53de Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 13:33:07 -0700 Subject: [PATCH 08/12] Use dummy_readline to prevent ImportErrors from readline breaking LE --- letsencrypt/display/completer.py | 6 +++++- letsencrypt/tests/display/completer_test.py | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 6b07a2e47..83dafa25d 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,6 +1,10 @@ """Provides tab completion when prompting users for a path.""" import glob -import readline +# readline module is not available on all systems +try: + import readline +except ImportError: + import letsencrypt.display.dummy_readline as readline class Completer(object): diff --git a/letsencrypt/tests/display/completer_test.py b/letsencrypt/tests/display/completer_test.py index a77d3c842..3c181c925 100644 --- a/letsencrypt/tests/display/completer_test.py +++ b/letsencrypt/tests/display/completer_test.py @@ -3,10 +3,12 @@ import os import readline import shutil import string +import sys import tempfile import unittest import mock +from six.moves import reload_module # pylint: disable=import-error class CompleterTest(unittest.TestCase): @@ -36,7 +38,6 @@ class CompleterTest(unittest.TestCase): def test_complete(self): from letsencrypt.display import completer - my_completer = completer.Completer() num_paths = len(self.paths) @@ -49,8 +50,17 @@ class CompleterTest(unittest.TestCase): completion = my_completer.complete(self.temp_dir, num_paths) self.assertEqual(completion, None) - def test_context_manager(self): + def test_import_error(self): + original_readline = sys.modules['readline'] + sys.modules['readline'] = None + + self.test_context_manager_with_unmocked_readline() + + sys.modules['readline'] = original_readline + + def test_context_manager_with_unmocked_readline(self): from letsencrypt.display import completer + reload_module(completer) original_completer = readline.get_completer() original_delims = readline.get_completer_delims() @@ -64,14 +74,14 @@ class CompleterTest(unittest.TestCase): @mock.patch('letsencrypt.display.completer.readline', autospec=True) def test_context_manager_libedit(self, mock_readline): mock_readline.__doc__ = 'libedit' - self._test_mocked_readline(mock_readline) + self._test_context_manager_with_mock_readline(mock_readline) @mock.patch('letsencrypt.display.completer.readline', autospec=True) def test_context_manager_readline(self, mock_readline): mock_readline.__doc__ = 'GNU readline' - self._test_mocked_readline(mock_readline) + self._test_context_manager_with_mock_readline(mock_readline) - def _test_mocked_readline(self, mock_readline): + def _test_context_manager_with_mock_readline(self, mock_readline): from letsencrypt.display import completer mock_readline.parse_and_bind.side_effect = enable_tab_completion From 9d70c4acfbc78fda0a2073f2d157a8e8bd9de84b Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:11:36 -0700 Subject: [PATCH 09/12] Add directory_select method to FileDisplay --- letsencrypt/display/util.py | 14 ++++++++++++++ letsencrypt/tests/display/util_test.py | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 05e280118..c94b0c921 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -7,6 +7,7 @@ import zope.interface from letsencrypt import interfaces from letsencrypt import errors +from letsencrypt.display import completer WIDTH = 72 HEIGHT = 20 @@ -339,6 +340,19 @@ class FileDisplay(object): else: return code, [] + def directory_select(self, message, **unused_kwargs): + """Display a directory selection screen. + + :param str message: prompt to give the user + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + with completer.Completer(): + return self.input(message) + def _scrub_checklist_input(self, indices, tags): # pylint: disable=no-self-use """Validate input and transform indices to appropriate tags. diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index adec265dc..32783b3bd 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -232,6 +232,15 @@ class FileOutputDisplayTest(unittest.TestCase): self.displayer._scrub_checklist_input(list_, TAGS)) self.assertEqual(set_tags, exp[i]) + @mock.patch("letsencrypt.display.util.FileDisplay.input") + def test_directory_select(self, mock_input): + message = "msg" + result = (display_util.OK, "/var/www/html",) + mock_input.return_value = result + + self.assertEqual(self.displayer.directory_select(message), result) + mock_input.assert_called_once_with(message) + def test_scrub_checklist_input_invalid(self): # pylint: disable=protected-access indices = [ From 294ea4d1a64be7be394854af177d35c60c2f1184 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:29:02 -0700 Subject: [PATCH 10/12] Add directory_select to NoninteractiveDisplay --- letsencrypt/display/util.py | 17 +++++++++++++++++ letsencrypt/tests/display/util_test.py | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index c94b0c921..005e2ba9c 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -555,6 +555,23 @@ class NoninteractiveDisplay(object): else: return OK, default + def directory_select(self, message, default=None, cli_flag=None): + """Simulate prompting the user for a directory. + + This function returns default if it is not ``None``, otherwise, + an exception is raised. + + :param str message: prompt to give the user + :param default: default value to return (if one exists) + :param str cli_flag: option used to set this value with the CLI + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + return self.input(message, default, cli_flag) + def separate_list_input(input_): """Separate a comma or space separated list. diff --git a/letsencrypt/tests/display/util_test.py b/letsencrypt/tests/display/util_test.py index 32783b3bd..bae0d582a 100644 --- a/letsencrypt/tests/display/util_test.py +++ b/letsencrypt/tests/display/util_test.py @@ -335,6 +335,15 @@ class NoninteractiveDisplayTest(unittest.TestCase): self.assertEqual(ret, (display_util.OK, d)) self.assertRaises(errors.MissingCommandlineFlag, self.displayer.checklist, "message", TAGS) + def test_directory_select(self): + default = "/var/www/html" + expected = (display_util.OK, default) + actual = self.displayer.directory_select("msg", default) + self.assertEqual(expected, actual) + + self.assertRaises( + errors.MissingCommandlineFlag, self.displayer.directory_select, "msg") + class SeparateListInputTest(unittest.TestCase): """Test Module functions.""" From 3031968ac5e148a857e057485f31dbbdbdbe0f39 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Wed, 23 Mar 2016 15:32:53 -0700 Subject: [PATCH 11/12] Add directory_select to IDisplay interface --- letsencrypt/interfaces.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 1921b1e54..188a4d9da 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -443,6 +443,20 @@ class IDisplay(zope.interface.Interface): """ + def directory_select(self, message, default=None, cli_flag=None): + """Display a directory selection screen. + + :param str message: prompt to give the user + :param default: the default value to return, if one exists, when + using the NoninteractiveDisplay + :param str cli_flag: option used to set this value with the CLI + + :returns: tuple of the form (`code`, `string`) where + `code` - int display exit code + `string` - input entered by the user + + """ + class IValidator(zope.interface.Interface): """Configuration validator.""" From f520ca25565429141d44ed28e3b1fda705ea9682 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 25 Mar 2016 12:52:37 -0700 Subject: [PATCH 12/12] Address @schoen's review comments --- letsencrypt/display/completer.py | 18 +++++++++--------- letsencrypt/display/util.py | 6 ++++-- letsencrypt/interfaces.py | 4 +++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/letsencrypt/display/completer.py b/letsencrypt/display/completer.py index 83dafa25d..fed476bb3 100644 --- a/letsencrypt/display/completer.py +++ b/letsencrypt/display/completer.py @@ -1,4 +1,4 @@ -"""Provides tab completion when prompting users for a path.""" +"""Provides Tab completion when prompting users for a path.""" import glob # readline module is not available on all systems try: @@ -8,9 +8,9 @@ except ImportError: class Completer(object): - """Provides tab completion when prompting users for a path. + """Provides Tab completion when prompting users for a path. - This class is meant to be used with readline to provide tab + This class is meant to be used with readline to provide Tab completion for users entering paths. The complete method can be passed to readline.set_completer directly, however, this function works best as a context manager. For example: @@ -18,14 +18,14 @@ class Completer(object): with Completer(): raw_input() - In this example, tab completion will be available during the call to + In this example, Tab completion will be available during the call to raw_input above, however, readline will be restored to its previous state when exiting the body of the with statement. """ def __init__(self): - self._completer = self._delims = self._iter = None + self._iter = self._original_completer = self._original_delims = None def complete(self, text, state): """Provides path completion for use with readline. @@ -43,8 +43,8 @@ class Completer(object): return next(self._iter, None) def __enter__(self): - self._completer = readline.get_completer() - self._delims = readline.get_completer_delims() + self._original_completer = readline.get_completer() + self._original_delims = readline.get_completer_delims() readline.set_completer(self.complete) readline.set_completer_delims(' \t\n;') @@ -57,5 +57,5 @@ class Completer(object): readline.parse_and_bind('tab: complete') def __exit__(self, unused_type, unused_value, unused_traceback): - readline.set_completer_delims(self._delims) - readline.set_completer(self._completer) + readline.set_completer_delims(self._original_delims) + readline.set_completer(self._original_completer) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 005e2ba9c..20c6be156 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -13,7 +13,7 @@ WIDTH = 72 HEIGHT = 20 DSELECT_HELP = ( - "Use the arrow keys or tab to move between window elements. Space can be " + "Use the arrow keys or Tab to move between window elements. Space can be " "used to complete the input path with the selected element in the " "directory window. Pressing enter will select the currently highlighted " "button.") @@ -559,7 +559,9 @@ class NoninteractiveDisplay(object): """Simulate prompting the user for a directory. This function returns default if it is not ``None``, otherwise, - an exception is raised. + an exception is raised explaining the problem. If cli_flag is + not ``None``, the error message will include the flag that can + be used to set this value with the CLI. :param str message: prompt to give the user :param default: default value to return (if one exists) diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 188a4d9da..2fba11869 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -449,7 +449,9 @@ class IDisplay(zope.interface.Interface): :param str message: prompt to give the user :param default: the default value to return, if one exists, when using the NoninteractiveDisplay - :param str cli_flag: option used to set this value with the CLI + :param str cli_flag: option used to set this value with the CLI, + if one exists, to be included in error messages given by + NoninteractiveDisplay :returns: tuple of the form (`code`, `string`) where `code` - int display exit code