Skip to content

Commit db41cc3

Browse files
committed
Defer import of unittest
1 parent a1cbef5 commit db41cc3

File tree

3 files changed

+218
-212
lines changed

3 files changed

+218
-212
lines changed

cmd2/cmd2.py

Lines changed: 3 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
2323
Git repository on GitHub at https://github.com/python-cmd2/cmd2
2424
"""
25+
# many imports are lazy-loaded when they are needed
2526
import argparse
2627
import atexit
2728
import cmd
@@ -43,7 +44,6 @@
4344
import tempfile
4445
import traceback
4546
from typing import Callable, List, Optional, Union, Tuple
46-
import unittest
4747
from code import InteractiveConsole
4848

4949
import pyperclip
@@ -3174,6 +3174,8 @@ def run_transcript_tests(self, callargs):
31743174
31753175
:param callargs: List[str] - list of transcript test file names
31763176
"""
3177+
import unittest
3178+
from .transcript import Cmd2TestCase
31773179
class TestMyAppCase(Cmd2TestCase):
31783180
cmdapp = self
31793181

@@ -3416,216 +3418,6 @@ def restore(self):
34163418
setattr(self.obj, attrib, getattr(self, attrib))
34173419

34183420

3419-
class OutputTrap(object):
3420-
"""Instantiate an OutputTrap to divert/capture ALL stdout output. For use in transcript testing."""
3421-
3422-
def __init__(self):
3423-
self.contents = ''
3424-
3425-
def write(self, txt):
3426-
"""Add text to the internal contents.
3427-
3428-
:param txt: str
3429-
"""
3430-
self.contents += txt
3431-
3432-
def read(self):
3433-
"""Read from the internal contents and then clear them out.
3434-
3435-
:return: str - text from the internal contents
3436-
"""
3437-
result = self.contents
3438-
self.contents = ''
3439-
return result
3440-
3441-
3442-
class Cmd2TestCase(unittest.TestCase):
3443-
"""Subclass this, setting CmdApp, to make a unittest.TestCase class
3444-
that will execute the commands in a transcript file and expect the results shown.
3445-
See example.py"""
3446-
cmdapp = None
3447-
3448-
def fetchTranscripts(self):
3449-
self.transcripts = {}
3450-
for fileset in self.cmdapp.testfiles:
3451-
for fname in glob.glob(fileset):
3452-
tfile = open(fname)
3453-
self.transcripts[fname] = iter(tfile.readlines())
3454-
tfile.close()
3455-
if not len(self.transcripts):
3456-
raise Exception("No test files found - nothing to test.")
3457-
3458-
def setUp(self):
3459-
if self.cmdapp:
3460-
self.fetchTranscripts()
3461-
3462-
# Trap stdout
3463-
self._orig_stdout = self.cmdapp.stdout
3464-
self.cmdapp.stdout = OutputTrap()
3465-
3466-
def runTest(self): # was testall
3467-
if self.cmdapp:
3468-
its = sorted(self.transcripts.items())
3469-
for (fname, transcript) in its:
3470-
self._test_transcript(fname, transcript)
3471-
3472-
def _test_transcript(self, fname, transcript):
3473-
line_num = 0
3474-
finished = False
3475-
line = utils.strip_ansi(next(transcript))
3476-
line_num += 1
3477-
while not finished:
3478-
# Scroll forward to where actual commands begin
3479-
while not line.startswith(self.cmdapp.visible_prompt):
3480-
try:
3481-
line = utils.strip_ansi(next(transcript))
3482-
except StopIteration:
3483-
finished = True
3484-
break
3485-
line_num += 1
3486-
command = [line[len(self.cmdapp.visible_prompt):]]
3487-
line = next(transcript)
3488-
# Read the entirety of a multi-line command
3489-
while line.startswith(self.cmdapp.continuation_prompt):
3490-
command.append(line[len(self.cmdapp.continuation_prompt):])
3491-
try:
3492-
line = next(transcript)
3493-
except StopIteration:
3494-
raise (StopIteration,
3495-
'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num,
3496-
command[0])
3497-
)
3498-
line_num += 1
3499-
command = ''.join(command)
3500-
# Send the command into the application and capture the resulting output
3501-
# TODO: Should we get the return value and act if stop == True?
3502-
self.cmdapp.onecmd_plus_hooks(command)
3503-
result = self.cmdapp.stdout.read()
3504-
# Read the expected result from transcript
3505-
if utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
3506-
message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format(
3507-
fname, line_num, command, result)
3508-
self.assert_(not (result.strip()), message)
3509-
continue
3510-
expected = []
3511-
while not utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
3512-
expected.append(line)
3513-
try:
3514-
line = next(transcript)
3515-
except StopIteration:
3516-
finished = True
3517-
break
3518-
line_num += 1
3519-
expected = ''.join(expected)
3520-
3521-
# transform the expected text into a valid regular expression
3522-
expected = self._transform_transcript_expected(expected)
3523-
message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format(
3524-
fname, line_num, command, expected, result)
3525-
self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message)
3526-
3527-
def _transform_transcript_expected(self, s):
3528-
"""parse the string with slashed regexes into a valid regex
3529-
3530-
Given a string like:
3531-
3532-
Match a 10 digit phone number: /\d{3}-\d{3}-\d{4}/
3533-
3534-
Turn it into a valid regular expression which matches the literal text
3535-
of the string and the regular expression. We have to remove the slashes
3536-
because they differentiate between plain text and a regular expression.
3537-
Unless the slashes are escaped, in which case they are interpreted as
3538-
plain text, or there is only one slash, which is treated as plain text
3539-
also.
3540-
3541-
Check the tests in tests/test_transcript.py to see all the edge
3542-
cases.
3543-
"""
3544-
regex = ''
3545-
start = 0
3546-
3547-
while True:
3548-
(regex, first_slash_pos, start) = self._escaped_find(regex, s, start, False)
3549-
if first_slash_pos == -1:
3550-
# no more slashes, add the rest of the string and bail
3551-
regex += re.escape(s[start:])
3552-
break
3553-
else:
3554-
# there is a slash, add everything we have found so far
3555-
# add stuff before the first slash as plain text
3556-
regex += re.escape(s[start:first_slash_pos])
3557-
start = first_slash_pos+1
3558-
# and go find the next one
3559-
(regex, second_slash_pos, start) = self._escaped_find(regex, s, start, True)
3560-
if second_slash_pos > 0:
3561-
# add everything between the slashes (but not the slashes)
3562-
# as a regular expression
3563-
regex += s[start:second_slash_pos]
3564-
# and change where we start looking for slashed on the
3565-
# turn through the loop
3566-
start = second_slash_pos + 1
3567-
else:
3568-
# No closing slash, we have to add the first slash,
3569-
# and the rest of the text
3570-
regex += re.escape(s[start-1:])
3571-
break
3572-
return regex
3573-
3574-
@staticmethod
3575-
def _escaped_find(regex, s, start, in_regex):
3576-
"""
3577-
Find the next slash in {s} after {start} that is not preceded by a backslash.
3578-
3579-
If we find an escaped slash, add everything up to and including it to regex,
3580-
updating {start}. {start} therefore serves two purposes, tells us where to start
3581-
looking for the next thing, and also tells us where in {s} we have already
3582-
added things to {regex}
3583-
3584-
{in_regex} specifies whether we are currently searching in a regex, we behave
3585-
differently if we are or if we aren't.
3586-
"""
3587-
3588-
while True:
3589-
pos = s.find('/', start)
3590-
if pos == -1:
3591-
# no match, return to caller
3592-
break
3593-
elif pos == 0:
3594-
# slash at the beginning of the string, so it can't be
3595-
# escaped. We found it.
3596-
break
3597-
else:
3598-
# check if the slash is preceeded by a backslash
3599-
if s[pos-1:pos] == '\\':
3600-
# it is.
3601-
if in_regex:
3602-
# add everything up to the backslash as a
3603-
# regular expression
3604-
regex += s[start:pos-1]
3605-
# skip the backslash, and add the slash
3606-
regex += s[pos]
3607-
else:
3608-
# add everything up to the backslash as escaped
3609-
# plain text
3610-
regex += re.escape(s[start:pos-1])
3611-
# and then add the slash as escaped
3612-
# plain text
3613-
regex += re.escape(s[pos])
3614-
# update start to show we have handled everything
3615-
# before it
3616-
start = pos+1
3617-
# and continue to look
3618-
else:
3619-
# slash is not escaped, this is what we are looking for
3620-
break
3621-
return regex, pos, start
3622-
3623-
def tearDown(self):
3624-
if self.cmdapp:
3625-
# Restore stdout
3626-
self.cmdapp.stdout = self._orig_stdout
3627-
3628-
36293421
def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')):
36303422
"""Wrapper around namedtuple which lets you treat the last value as optional.
36313423

0 commit comments

Comments
 (0)