Skip to content

Commit fc495a4

Browse files
committed
Move more code from cmd2.py into utils.py
1 parent 09abad2 commit fc495a4

File tree

3 files changed

+119
-127
lines changed

3 files changed

+119
-127
lines changed

cmd2/cmd2.py

Lines changed: 4 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,6 @@ def categorize(func: Union[Callable, Iterable], category: str) -> None:
125125
else:
126126
setattr(func, HELP_CATEGORY, category)
127127

128-
129-
def _which(editor: str) -> Optional[str]:
130-
import subprocess
131-
try:
132-
editor_path = subprocess.check_output(['which', editor], stderr=subprocess.STDOUT).strip()
133-
editor_path = editor_path.decode()
134-
except subprocess.CalledProcessError:
135-
editor_path = None
136-
return editor_path
137-
138-
139128
def parse_quoted_string(cmdline: str) -> List[str]:
140129
"""Parse a quoted string into a list of arguments."""
141130
if isinstance(cmdline, list):
@@ -347,7 +336,7 @@ class Cmd(cmd.Cmd):
347336
else:
348337
# Favor command-line editors first so we don't leave the terminal to edit
349338
for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']:
350-
if _which(editor):
339+
if utils.which(editor):
351340
break
352341
feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
353342
locals_in_py = False
@@ -2437,7 +2426,7 @@ def do_set(self, args):
24372426
if (val[0] == val[-1]) and val[0] in ("'", '"'):
24382427
val = val[1:-1]
24392428
else:
2440-
val = cast(current_val, val)
2429+
val = utils.cast(current_val, val)
24412430
setattr(self, param_name, val)
24422431
self.poutput('%s - was: %s\nnow: %s\n' % (param_name, current_val, val))
24432432
if current_val != val:
@@ -2865,7 +2854,7 @@ def do_load(self, arglist):
28652854
return
28662855

28672856
# Make sure the file is ASCII or UTF-8 encoded text
2868-
if not self.is_text_file(expanded_path):
2857+
if not utils.is_text_file(expanded_path):
28692858
self.perror('{} is not an ASCII or UTF-8 encoded text file'.format(expanded_path), traceback_war=False)
28702859
return
28712860

@@ -2886,42 +2875,6 @@ def complete_load(self, text, line, begidx, endidx):
28862875
index_dict = {1: self.path_complete}
28872876
return self.index_based_complete(text, line, begidx, endidx, index_dict)
28882877

2889-
@staticmethod
2890-
def is_text_file(file_path):
2891-
"""
2892-
Returns if a file contains only ASCII or UTF-8 encoded text
2893-
:param file_path: path to the file being checked
2894-
"""
2895-
import codecs
2896-
2897-
expanded_path = os.path.abspath(os.path.expanduser(file_path.strip()))
2898-
valid_text_file = False
2899-
2900-
# Check if the file is ASCII
2901-
try:
2902-
with codecs.open(expanded_path, encoding='ascii', errors='strict') as f:
2903-
# Make sure the file has at least one line of text
2904-
# noinspection PyUnusedLocal
2905-
if sum(1 for line in f) > 0:
2906-
valid_text_file = True
2907-
except IOError: # pragma: no cover
2908-
pass
2909-
except UnicodeDecodeError:
2910-
# The file is not ASCII. Check if it is UTF-8.
2911-
try:
2912-
with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f:
2913-
# Make sure the file has at least one line of text
2914-
# noinspection PyUnusedLocal
2915-
if sum(1 for line in f) > 0:
2916-
valid_text_file = True
2917-
except IOError: # pragma: no cover
2918-
pass
2919-
except UnicodeDecodeError:
2920-
# Not UTF-8
2921-
pass
2922-
2923-
return valid_text_file
2924-
29252878
def run_transcript_tests(self, callargs):
29262879
"""Runs transcript tests for provided file(s).
29272880
@@ -3119,36 +3072,6 @@ def isin(hi):
31193072
return [itm for itm in self if isin(itm)]
31203073

31213074

3122-
def cast(current, new):
3123-
"""Tries to force a new value into the same type as the current when trying to set the value for a parameter.
3124-
3125-
:param current: current value for the parameter, type varies
3126-
:param new: str - new value
3127-
:return: new value with same type as current, or the current value if there was an error casting
3128-
"""
3129-
typ = type(current)
3130-
if typ == bool:
3131-
try:
3132-
return bool(int(new))
3133-
except (ValueError, TypeError):
3134-
pass
3135-
try:
3136-
new = new.lower()
3137-
except AttributeError:
3138-
pass
3139-
if (new == 'on') or (new[0] in ('y', 't')):
3140-
return True
3141-
if (new == 'off') or (new[0] in ('n', 'f')):
3142-
return False
3143-
else:
3144-
try:
3145-
return typ(new)
3146-
except (ValueError, TypeError):
3147-
pass
3148-
print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new))
3149-
return current
3150-
3151-
31523075
class Statekeeper(object):
31533076
"""Class used to save and restore state during load and py commands as well as when redirecting output or pipes."""
31543077
def __init__(self, obj, attribs):
@@ -3174,22 +3097,7 @@ def restore(self):
31743097
setattr(self.obj, attrib, getattr(self, attrib))
31753098

31763099

3177-
def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')):
3178-
"""Wrapper around namedtuple which lets you treat the last value as optional.
3179-
3180-
:param typename: str - type name for the Named tuple
3181-
:param field_names: List[str] or space-separated string of field names
3182-
:param default_values: (optional) 2-element tuple containing the default values for last 2 parameters in named tuple
3183-
Defaults to an empty string for both of them
3184-
:return: namedtuple type
3185-
"""
3186-
T = collections.namedtuple(typename, field_names)
3187-
# noinspection PyUnresolvedReferences
3188-
T.__new__.__defaults__ = default_values
3189-
return T
3190-
3191-
3192-
class CmdResult(namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war'])):
3100+
class CmdResult(utils.namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war'])):
31933101
"""Derive a class to store results from a named tuple so we can tweak dunder methods for convenience.
31943102
31953103
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'])
32093117
def __bool__(self):
32103118
"""If err is an empty string, treat the result as a success; otherwise treat it as a failure."""
32113119
return not self.err
3212-
3213-

cmd2/utils.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"""Shared utility functions"""
44

55
import collections
6+
import os
7+
from typing import Optional
8+
69
from . import constants
710

811
def strip_ansi(text: str) -> str:
@@ -55,3 +58,89 @@ def namedtuple_with_defaults(typename, field_names, default_values=()):
5558
T.__new__.__defaults__ = tuple(prototype)
5659
return T
5760

61+
def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')):
62+
"""Wrapper around namedtuple which lets you treat the last value as optional.
63+
64+
:param typename: str - type name for the Named tuple
65+
:param field_names: List[str] or space-separated string of field names
66+
:param default_values: (optional) 2-element tuple containing the default values for last 2 parameters in named tuple
67+
Defaults to an empty string for both of them
68+
:return: namedtuple type
69+
"""
70+
T = collections.namedtuple(typename, field_names)
71+
# noinspection PyUnresolvedReferences
72+
T.__new__.__defaults__ = default_values
73+
return T
74+
75+
def cast(current, new):
76+
"""Tries to force a new value into the same type as the current when trying to set the value for a parameter.
77+
78+
:param current: current value for the parameter, type varies
79+
:param new: str - new value
80+
:return: new value with same type as current, or the current value if there was an error casting
81+
"""
82+
typ = type(current)
83+
if typ == bool:
84+
try:
85+
return bool(int(new))
86+
except (ValueError, TypeError):
87+
pass
88+
try:
89+
new = new.lower()
90+
except AttributeError:
91+
pass
92+
if (new == 'on') or (new[0] in ('y', 't')):
93+
return True
94+
if (new == 'off') or (new[0] in ('n', 'f')):
95+
return False
96+
else:
97+
try:
98+
return typ(new)
99+
except (ValueError, TypeError):
100+
pass
101+
print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new))
102+
return current
103+
104+
def which(editor: str) -> Optional[str]:
105+
import subprocess
106+
try:
107+
editor_path = subprocess.check_output(['which', editor], stderr=subprocess.STDOUT).strip()
108+
editor_path = editor_path.decode()
109+
except subprocess.CalledProcessError:
110+
editor_path = None
111+
return editor_path
112+
113+
def is_text_file(file_path):
114+
"""
115+
Returns if a file contains only ASCII or UTF-8 encoded text
116+
:param file_path: path to the file being checked
117+
"""
118+
import codecs
119+
120+
expanded_path = os.path.abspath(os.path.expanduser(file_path.strip()))
121+
valid_text_file = False
122+
123+
# Check if the file is ASCII
124+
try:
125+
with codecs.open(expanded_path, encoding='ascii', errors='strict') as f:
126+
# Make sure the file has at least one line of text
127+
# noinspection PyUnusedLocal
128+
if sum(1 for line in f) > 0:
129+
valid_text_file = True
130+
except IOError: # pragma: no cover
131+
pass
132+
except UnicodeDecodeError:
133+
# The file is not ASCII. Check if it is UTF-8.
134+
try:
135+
with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f:
136+
# Make sure the file has at least one line of text
137+
# noinspection PyUnusedLocal
138+
if sum(1 for line in f) > 0:
139+
valid_text_file = True
140+
except IOError: # pragma: no cover
141+
pass
142+
except UnicodeDecodeError:
143+
# Not UTF-8
144+
pass
145+
146+
return valid_text_file

tests/test_cmd2.py

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import argparse
99
import builtins
1010
from code import InteractiveConsole
11+
import io
1112
import os
1213
import sys
13-
import io
1414
import tempfile
1515

1616
import pytest
@@ -22,6 +22,7 @@
2222
from unittest import mock
2323

2424
from cmd2 import cmd2
25+
from cmd2 import utils
2526
from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
2627
HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut
2728

@@ -109,44 +110,40 @@ def test_base_show_readonly(base_app):
109110

110111

111112
def test_cast():
112-
cast = cmd2.cast
113-
114113
# Boolean
115-
assert cast(True, True) == True
116-
assert cast(True, False) == False
117-
assert cast(True, 0) == False
118-
assert cast(True, 1) == True
119-
assert cast(True, 'on') == True
120-
assert cast(True, 'off') == False
121-
assert cast(True, 'ON') == True
122-
assert cast(True, 'OFF') == False
123-
assert cast(True, 'y') == True
124-
assert cast(True, 'n') == False
125-
assert cast(True, 't') == True
126-
assert cast(True, 'f') == False
114+
assert utils.cast(True, True) == True
115+
assert utils.cast(True, False) == False
116+
assert utils.cast(True, 0) == False
117+
assert utils.cast(True, 1) == True
118+
assert utils.cast(True, 'on') == True
119+
assert utils.cast(True, 'off') == False
120+
assert utils.cast(True, 'ON') == True
121+
assert utils.cast(True, 'OFF') == False
122+
assert utils.cast(True, 'y') == True
123+
assert utils.cast(True, 'n') == False
124+
assert utils.cast(True, 't') == True
125+
assert utils.cast(True, 'f') == False
127126

128127
# Non-boolean same type
129-
assert cast(1, 5) == 5
130-
assert cast(3.4, 2.7) == 2.7
131-
assert cast('foo', 'bar') == 'bar'
132-
assert cast([1,2], [3,4]) == [3,4]
128+
assert utils.cast(1, 5) == 5
129+
assert utils.cast(3.4, 2.7) == 2.7
130+
assert utils.cast('foo', 'bar') == 'bar'
131+
assert utils.cast([1,2], [3,4]) == [3,4]
133132

134133
def test_cast_problems(capsys):
135-
cast = cmd2.cast
136-
137134
expected = 'Problem setting parameter (now {}) to {}; incorrect type?\n'
138135

139136
# Boolean current, with new value not convertible to bool
140137
current = True
141138
new = [True, True]
142-
assert cast(current, new) == current
139+
assert utils.cast(current, new) == current
143140
out, err = capsys.readouterr()
144141
assert out == expected.format(current, new)
145142

146143
# Non-boolean current, with new value not convertible to current type
147144
current = 1
148145
new = 'octopus'
149-
assert cast(current, new) == current
146+
assert utils.cast(current, new) == current
150147
out, err = capsys.readouterr()
151148
assert out == expected.format(current, new)
152149

@@ -1365,18 +1362,18 @@ def test_help_with_no_docstring(capsys):
13651362
"""
13661363

13671364
@pytest.mark.skipif(sys.platform.startswith('win'),
1368-
reason="cmd2._which function only used on Mac and Linux")
1365+
reason="utils.which function only used on Mac and Linux")
13691366
def test_which_editor_good():
13701367
editor = 'vi'
1371-
path = cmd2._which(editor)
1368+
path = utils.which(editor)
13721369
# Assert that the vi editor was found because it should exist on all Mac and Linux systems
13731370
assert path
13741371

13751372
@pytest.mark.skipif(sys.platform.startswith('win'),
1376-
reason="cmd2._which function only used on Mac and Linux")
1373+
reason="utils.which function only used on Mac and Linux")
13771374
def test_which_editor_bad():
13781375
editor = 'notepad.exe'
1379-
path = cmd2._which(editor)
1376+
path = utils.which(editor)
13801377
# Assert that the editor wasn't found because no notepad.exe on non-Windows systems ;-)
13811378
assert path is None
13821379

@@ -1463,11 +1460,11 @@ def test_cmdresult(cmdresult_app):
14631460

14641461
def test_is_text_file_bad_input(base_app):
14651462
# Test with a non-existent file
1466-
file_is_valid = base_app.is_text_file('does_not_exist.txt')
1463+
file_is_valid = utils.is_text_file('does_not_exist.txt')
14671464
assert not file_is_valid
14681465

14691466
# Test with a directory
1470-
dir_is_valid = base_app.is_text_file('.')
1467+
dir_is_valid = utils.is_text_file('.')
14711468
assert not dir_is_valid
14721469

14731470

0 commit comments

Comments
 (0)