summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcmd2/cmd2.py102
-rw-r--r--cmd2/utils.py89
-rw-r--r--tests/test_cmd2.py55
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