From c49b1fa8d0a3ed77807d57a311ea63a362e4dd1a Mon Sep 17 00:00:00 2001 From: BJ Dierkes Date: Fri, 8 Aug 2014 15:17:25 -0500 Subject: [PATCH] Resolves Issue #119 (with tests) --- cement/utils/shell.py | 32 +++++++- requirements-dev-py3.txt | 1 + requirements-dev.txt | 1 + tests/utils/shell_tests.py | 150 +++++++++++++++++++++++++++++++++---- 4 files changed, 167 insertions(+), 17 deletions(-) diff --git a/cement/utils/shell.py b/cement/utils/shell.py index 3df829d5..afc70eab 100644 --- a/cement/utils/shell.py +++ b/cement/utils/shell.py @@ -1,10 +1,13 @@ """Common Shell Utilities.""" import os +import sys from subprocess import Popen, PIPE from multiprocessing import Process from threading import Thread from ..core.meta import MetaMixin +from ..core.exc import FrameworkError + def exec_cmd(cmd_args, *args, **kw): """ @@ -142,9 +145,10 @@ def spawn_thread(target, start=True, join=False, *args, **kwargs): class Prompt(MetaMixin): """ - A wrapper around `raw_input` whose purpose is to limit the redundent - tasks of gather user input. Can be used in several ways depending on the - user case (simple input, options, and numbered selection). + A wrapper around `raw_input` or `input` (py3) whose purpose is to limit + the redundent tasks of gather user input. Can be used in several ways + depending on the user case (simple input, options, and numbered + selection). :param text: The text displayed at the input prompt. @@ -268,6 +272,13 @@ class Prompt(MetaMixin): #: Command to issue when clearing the terminal. clear_command = 'clear' + #: Max attempts to get proper input from the user before giving up. + max_attempts = 10 + + #: Raise an exception when max_attempts is hit? If not, Prompt + #: passes the input through as `None`. + max_attempts_exception = True + def __init__(self, text=None, *args, **kw): if text is not None: kw['text'] = text @@ -298,7 +309,11 @@ class Prompt(MetaMixin): else: text = self._meta.text - self.input = raw_input("%s " % text) + if sys.version_info[0] < 3: # pragma: nocover + self.input = raw_input("%s " % text) # pragma: nocover + else: # pragma: nocover + self.input = input("%s " % text) # pragma: nocover + if self.input == '' and self._meta.default is not None: self.input = self._meta.default elif self.input == '': @@ -309,7 +324,16 @@ class Prompt(MetaMixin): Prompt the user, and store their input as `self.input`. """ + attempt = 0 while self.input is None: + if attempt >= int(self._meta.max_attempts): + if self._meta.max_attempts_exception is True: + raise FrameworkError("Maximum attempts exceeded getting " + "valid user input") + else: + return self.input + + attempt += 1 self._prompt() if self.input is None: diff --git a/requirements-dev-py3.txt b/requirements-dev-py3.txt index bd68d30f..e91d67f8 100644 --- a/requirements-dev-py3.txt +++ b/requirements-dev-py3.txt @@ -4,6 +4,7 @@ coverage sphinx pep8 autopep8 +mock # Required for optional extensions (only the ones supported on py3) pystache diff --git a/requirements-dev.txt b/requirements-dev.txt index 0c009a2e..923457f6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,7 @@ coverage sphinx pep8 autopep8 +mock # Required for optional extensions pystache diff --git a/tests/utils/shell_tests.py b/tests/utils/shell_tests.py index 715ae83a..7a11c002 100644 --- a/tests/utils/shell_tests.py +++ b/tests/utils/shell_tests.py @@ -1,59 +1,183 @@ """Tests for cement.utils.shell""" +import sys import time +import mock from cement.utils import shell, test +from cement.core.exc import FrameworkError + +if sys.version_info[0] < 3: + INPUT = '__builtin__.raw_input' +else: + INPUT = 'builtins.input' def add(a, b): return a + b - + + class ShellUtilsTestCase(test.CementCoreTestCase): def test_exec_cmd(self): out, err, ret = shell.exec_cmd(['echo', 'KAPLA!']) self.eq(ret, 0) self.eq(out, b'KAPLA!\n') - + def test_exec_cmd_shell_true(self): out, err, ret = shell.exec_cmd(['echo KAPLA!'], shell=True) self.eq(ret, 0) self.eq(out, b'KAPLA!\n') - + def test_exec_cmd2(self): ret = shell.exec_cmd2(['echo']) self.eq(ret, 0) - + def test_exec_cmd2_shell_true(self): ret = shell.exec_cmd2(['echo johnny'], shell=True) self.eq(ret, 0) - + def test_exec_cmd_bad_command(self): out, err, ret = shell.exec_cmd(['false']) self.eq(ret, 1) - + def test_exec_cmd2_bad_command(self): ret = shell.exec_cmd2(['false']) self.eq(ret, 1) - + def test_spawn_process(self): p = shell.spawn_process(add, args=(23, 2)) p.join() self.eq(p.exitcode, 0) - + p = shell.spawn_process(add, join=True, args=(23, 2)) self.eq(p.exitcode, 0) - + def test_spawn_thread(self): t = shell.spawn_thread(time.sleep, args=(10)) - + # before joining it is alive res = t.is_alive() self.eq(res, True) - + t.join() - + # after joining it is not alive res = t.is_alive() self.eq(res, False) - + t = shell.spawn_thread(time.sleep, join=True, args=(10)) res = t.is_alive() self.eq(res, False) + + def test_prompt_simple(self): + with mock.patch(INPUT, return_value='Test Input'): + p = shell.Prompt("Test Prompt") + self.eq(p.input, 'Test Input') + + def test_prompt_clear(self): + # test with a non-clear command: + with mock.patch(INPUT, return_value='Test Input'): + p = shell.Prompt("Test Prompt", + clear=True, + clear_command='true', + ) + self.eq(p.input, 'Test Input') + + def test_prompt_options(self): + # test options (non-numbered.. user inputs actual option) + with mock.patch(INPUT, return_value='y'): + p = shell.Prompt("Test Prompt", options=['y', 'n']) + self.eq(p.input, 'y') + + # test default value + with mock.patch(INPUT, return_value=''): + p = shell.Prompt("Test Prompt", options=['y', 'n'], default='n') + self.eq(p.input, 'n') + + def test_prompt_numbered_options(self): + # test numbered selection (user inputs number) + with mock.patch(INPUT, return_value='3'): + p = shell.Prompt("Test Prompt", + options=['yes', 'no', 'maybe'], + numbered=True, + ) + self.eq(p.input, 'maybe') + + # test default value + with mock.patch(INPUT, return_value=''): + p = shell.Prompt( + "Test Prompt", + options=['yes', 'no', 'maybe'], + numbered=True, + default='2', + ) + self.eq(p.input, 'no') + + def test_prompt_input_is_none(self): + # test that self.input is none if no default, and no input + with mock.patch(INPUT, return_value=''): + p = shell.Prompt('Test Prompt', + max_attempts=3, + max_attempts_exception=False, + ) + self.eq(p.input, None) + + @test.raises(FrameworkError) + def test_prompt_max_attempts(self): + # test that self.input is none if no default, and no input + with mock.patch(INPUT, return_value=''): + try: + p = shell.Prompt('Test Prompt', + max_attempts=3, + max_attempts_exception=True, + ) + except FrameworkError as e: + self.eq(e.msg, + "Maximum attempts exceeded getting valid user input", + ) + raise + + def test_prompt_index_and_value_errors(self): + with mock.patch(INPUT, return_value='5'): + p = shell.Prompt( + "Test Prompt", + options=['yes', 'no', 'maybe'], + numbered=True, + max_attempts=3, + max_attempts_exception=False, + ) + self.eq(p.input, None) + + def test_prompt_case_insensitive(self): + with mock.patch(INPUT, return_value='NO'): + p = shell.Prompt( + "Test Prompt", + options=['yes', 'no', 'maybe'], + case_insensitive=True, + ) + self.eq(p.input, 'NO') + + with mock.patch(INPUT, return_value='NOT VALID'): + p = shell.Prompt( + "Test Prompt", + options=['yes', 'no', 'maybe'], + case_insensitive=True, + max_attempts=3, + max_attempts_exception=False, + ) + self.eq(p.input, None) + + def test_prompt_case_sensitive(self): + with mock.patch(INPUT, return_value='NO'): + p = shell.Prompt( + "Test Prompt", + options=['yes', 'no', 'maybe'], + case_insensitive=False, + max_attempts=3, + max_attempts_exception=False, + ) + self.eq(p.input, None) + + + + + +