diff options
-rwxr-xr-x | cmd2/cmd2.py | 102 | ||||
-rw-r--r-- | cmd2/utils.py | 89 | ||||
-rw-r--r-- | tests/test_cmd2.py | 55 |
3 files changed, 119 insertions, 127 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c90d7dab..7547c012 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -125,17 +125,6 @@ def categorize(func: Union[Callable, Iterable], category: str) -> None: else: setattr(func, HELP_CATEGORY, category) - -def _which(editor: str) -> Optional[str]: - import subprocess - try: - editor_path = subprocess.check_output(['which', editor], stderr=subprocess.STDOUT).strip() - editor_path = editor_path.decode() - except subprocess.CalledProcessError: - editor_path = None - return editor_path - - def parse_quoted_string(cmdline: str) -> List[str]: """Parse a quoted string into a list of arguments.""" if isinstance(cmdline, list): @@ -347,7 +336,7 @@ class Cmd(cmd.Cmd): else: # Favor command-line editors first so we don't leave the terminal to edit for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']: - if _which(editor): + if utils.which(editor): break feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing) locals_in_py = False @@ -2437,7 +2426,7 @@ Usage: Usage: unalias [-a] name [name ...] if (val[0] == val[-1]) and val[0] in ("'", '"'): val = val[1:-1] else: - val = cast(current_val, val) + val = utils.cast(current_val, val) setattr(self, param_name, val) self.poutput('%s - was: %s\nnow: %s\n' % (param_name, current_val, val)) if current_val != val: @@ -2865,7 +2854,7 @@ Script should contain one command per line, just like command would be typed in return # Make sure the file is ASCII or UTF-8 encoded text - if not self.is_text_file(expanded_path): + if not utils.is_text_file(expanded_path): self.perror('{} is not an ASCII or UTF-8 encoded text file'.format(expanded_path), traceback_war=False) return @@ -2886,42 +2875,6 @@ Script should contain one command per line, just like command would be typed in index_dict = {1: self.path_complete} return self.index_based_complete(text, line, begidx, endidx, index_dict) - @staticmethod - def is_text_file(file_path): - """ - Returns if a file contains only ASCII or UTF-8 encoded text - :param file_path: path to the file being checked - """ - import codecs - - expanded_path = os.path.abspath(os.path.expanduser(file_path.strip())) - valid_text_file = False - - # Check if the file is ASCII - try: - with codecs.open(expanded_path, encoding='ascii', errors='strict') as f: - # Make sure the file has at least one line of text - # noinspection PyUnusedLocal - if sum(1 for line in f) > 0: - valid_text_file = True - except IOError: # pragma: no cover - pass - except UnicodeDecodeError: - # The file is not ASCII. Check if it is UTF-8. - try: - with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f: - # Make sure the file has at least one line of text - # noinspection PyUnusedLocal - if sum(1 for line in f) > 0: - valid_text_file = True - except IOError: # pragma: no cover - pass - except UnicodeDecodeError: - # Not UTF-8 - pass - - return valid_text_file - def run_transcript_tests(self, callargs): """Runs transcript tests for provided file(s). @@ -3119,36 +3072,6 @@ class History(list): return [itm for itm in self if isin(itm)] -def cast(current, new): - """Tries to force a new value into the same type as the current when trying to set the value for a parameter. - - :param current: current value for the parameter, type varies - :param new: str - new value - :return: new value with same type as current, or the current value if there was an error casting - """ - typ = type(current) - if typ == bool: - try: - return bool(int(new)) - except (ValueError, TypeError): - pass - try: - new = new.lower() - except AttributeError: - pass - if (new == 'on') or (new[0] in ('y', 't')): - return True - if (new == 'off') or (new[0] in ('n', 'f')): - return False - else: - try: - return typ(new) - except (ValueError, TypeError): - pass - print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new)) - return current - - class Statekeeper(object): """Class used to save and restore state during load and py commands as well as when redirecting output or pipes.""" def __init__(self, obj, attribs): @@ -3174,22 +3097,7 @@ class Statekeeper(object): setattr(self.obj, attrib, getattr(self, attrib)) -def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')): - """Wrapper around namedtuple which lets you treat the last value as optional. - - :param typename: str - type name for the Named tuple - :param field_names: List[str] or space-separated string of field names - :param default_values: (optional) 2-element tuple containing the default values for last 2 parameters in named tuple - Defaults to an empty string for both of them - :return: namedtuple type - """ - T = collections.namedtuple(typename, field_names) - # noinspection PyUnresolvedReferences - T.__new__.__defaults__ = default_values - return T - - -class CmdResult(namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war'])): +class CmdResult(utils.namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war'])): """Derive a class to store results from a named tuple so we can tweak dunder methods for convenience. This is provided as a convenience and an example for one possible way for end users to store results in @@ -3209,5 +3117,3 @@ class CmdResult(namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war']) def __bool__(self): """If err is an empty string, treat the result as a success; otherwise treat it as a failure.""" return not self.err - - diff --git a/cmd2/utils.py b/cmd2/utils.py index dbe39213..a61fd5fd 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -3,6 +3,9 @@ """Shared utility functions""" import collections +import os +from typing import Optional + from . import constants def strip_ansi(text: str) -> str: @@ -55,3 +58,89 @@ def namedtuple_with_defaults(typename, field_names, default_values=()): T.__new__.__defaults__ = tuple(prototype) return T +def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')): + """Wrapper around namedtuple which lets you treat the last value as optional. + + :param typename: str - type name for the Named tuple + :param field_names: List[str] or space-separated string of field names + :param default_values: (optional) 2-element tuple containing the default values for last 2 parameters in named tuple + Defaults to an empty string for both of them + :return: namedtuple type + """ + T = collections.namedtuple(typename, field_names) + # noinspection PyUnresolvedReferences + T.__new__.__defaults__ = default_values + return T + +def cast(current, new): + """Tries to force a new value into the same type as the current when trying to set the value for a parameter. + + :param current: current value for the parameter, type varies + :param new: str - new value + :return: new value with same type as current, or the current value if there was an error casting + """ + typ = type(current) + if typ == bool: + try: + return bool(int(new)) + except (ValueError, TypeError): + pass + try: + new = new.lower() + except AttributeError: + pass + if (new == 'on') or (new[0] in ('y', 't')): + return True + if (new == 'off') or (new[0] in ('n', 'f')): + return False + else: + try: + return typ(new) + except (ValueError, TypeError): + pass + print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new)) + return current + +def which(editor: str) -> Optional[str]: + import subprocess + try: + editor_path = subprocess.check_output(['which', editor], stderr=subprocess.STDOUT).strip() + editor_path = editor_path.decode() + except subprocess.CalledProcessError: + editor_path = None + return editor_path + +def is_text_file(file_path): + """ + Returns if a file contains only ASCII or UTF-8 encoded text + :param file_path: path to the file being checked + """ + import codecs + + expanded_path = os.path.abspath(os.path.expanduser(file_path.strip())) + valid_text_file = False + + # Check if the file is ASCII + try: + with codecs.open(expanded_path, encoding='ascii', errors='strict') as f: + # Make sure the file has at least one line of text + # noinspection PyUnusedLocal + if sum(1 for line in f) > 0: + valid_text_file = True + except IOError: # pragma: no cover + pass + except UnicodeDecodeError: + # The file is not ASCII. Check if it is UTF-8. + try: + with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f: + # Make sure the file has at least one line of text + # noinspection PyUnusedLocal + if sum(1 for line in f) > 0: + valid_text_file = True + except IOError: # pragma: no cover + pass + except UnicodeDecodeError: + # Not UTF-8 + pass + + return valid_text_file diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 0da7e9d5..11c2cad8 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -8,9 +8,9 @@ Released under MIT license, see LICENSE file import argparse import builtins from code import InteractiveConsole +import io import os import sys -import io import tempfile import pytest @@ -22,6 +22,7 @@ except ImportError: from unittest import mock from cmd2 import cmd2 +from cmd2 import utils from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \ HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut @@ -109,44 +110,40 @@ def test_base_show_readonly(base_app): def test_cast(): - cast = cmd2.cast - # Boolean - assert cast(True, True) == True - assert cast(True, False) == False - assert cast(True, 0) == False - assert cast(True, 1) == True - assert cast(True, 'on') == True - assert cast(True, 'off') == False - assert cast(True, 'ON') == True - assert cast(True, 'OFF') == False - assert cast(True, 'y') == True - assert cast(True, 'n') == False - assert cast(True, 't') == True - assert cast(True, 'f') == False + assert utils.cast(True, True) == True + assert utils.cast(True, False) == False + assert utils.cast(True, 0) == False + assert utils.cast(True, 1) == True + assert utils.cast(True, 'on') == True + assert utils.cast(True, 'off') == False + assert utils.cast(True, 'ON') == True + assert utils.cast(True, 'OFF') == False + assert utils.cast(True, 'y') == True + assert utils.cast(True, 'n') == False + assert utils.cast(True, 't') == True + assert utils.cast(True, 'f') == False # Non-boolean same type - assert cast(1, 5) == 5 - assert cast(3.4, 2.7) == 2.7 - assert cast('foo', 'bar') == 'bar' - assert cast([1,2], [3,4]) == [3,4] + assert utils.cast(1, 5) == 5 + assert utils.cast(3.4, 2.7) == 2.7 + assert utils.cast('foo', 'bar') == 'bar' + assert utils.cast([1,2], [3,4]) == [3,4] def test_cast_problems(capsys): - cast = cmd2.cast - expected = 'Problem setting parameter (now {}) to {}; incorrect type?\n' # Boolean current, with new value not convertible to bool current = True new = [True, True] - assert cast(current, new) == current + assert utils.cast(current, new) == current out, err = capsys.readouterr() assert out == expected.format(current, new) # Non-boolean current, with new value not convertible to current type current = 1 new = 'octopus' - assert cast(current, new) == current + assert utils.cast(current, new) == current out, err = capsys.readouterr() assert out == expected.format(current, new) @@ -1365,18 +1362,18 @@ optional arguments: """ @pytest.mark.skipif(sys.platform.startswith('win'), - reason="cmd2._which function only used on Mac and Linux") + reason="utils.which function only used on Mac and Linux") def test_which_editor_good(): editor = 'vi' - path = cmd2._which(editor) + path = utils.which(editor) # Assert that the vi editor was found because it should exist on all Mac and Linux systems assert path @pytest.mark.skipif(sys.platform.startswith('win'), - reason="cmd2._which function only used on Mac and Linux") + reason="utils.which function only used on Mac and Linux") def test_which_editor_bad(): editor = 'notepad.exe' - path = cmd2._which(editor) + path = utils.which(editor) # Assert that the editor wasn't found because no notepad.exe on non-Windows systems ;-) assert path is None @@ -1463,11 +1460,11 @@ def test_cmdresult(cmdresult_app): def test_is_text_file_bad_input(base_app): # Test with a non-existent file - file_is_valid = base_app.is_text_file('does_not_exist.txt') + file_is_valid = utils.is_text_file('does_not_exist.txt') assert not file_is_valid # Test with a directory - dir_is_valid = base_app.is_text_file('.') + dir_is_valid = utils.is_text_file('.') assert not dir_is_valid |