From db41cc3743eb45f0c53a387265f8cf496bbacd29 Mon Sep 17 00:00:00 2001 From: kotfu Date: Sun, 6 May 2018 13:58:05 -0600 Subject: Defer import of unittest --- cmd2/cmd2.py | 214 +---------------------------------------------------------- 1 file changed, 3 insertions(+), 211 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 1378f052..9428afb3 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -22,6 +22,7 @@ is used in place of `print`. Git repository on GitHub at https://github.com/python-cmd2/cmd2 """ +# many imports are lazy-loaded when they are needed import argparse import atexit import cmd @@ -43,7 +44,6 @@ import sys import tempfile import traceback from typing import Callable, List, Optional, Union, Tuple -import unittest from code import InteractiveConsole import pyperclip @@ -3174,6 +3174,8 @@ Script should contain one command per line, just like command would be typed in :param callargs: List[str] - list of transcript test file names """ + import unittest + from .transcript import Cmd2TestCase class TestMyAppCase(Cmd2TestCase): cmdapp = self @@ -3416,216 +3418,6 @@ class Statekeeper(object): setattr(self.obj, attrib, getattr(self, attrib)) -class OutputTrap(object): - """Instantiate an OutputTrap to divert/capture ALL stdout output. For use in transcript testing.""" - - def __init__(self): - self.contents = '' - - def write(self, txt): - """Add text to the internal contents. - - :param txt: str - """ - self.contents += txt - - def read(self): - """Read from the internal contents and then clear them out. - - :return: str - text from the internal contents - """ - result = self.contents - self.contents = '' - return result - - -class Cmd2TestCase(unittest.TestCase): - """Subclass this, setting CmdApp, to make a unittest.TestCase class - that will execute the commands in a transcript file and expect the results shown. - See example.py""" - cmdapp = None - - def fetchTranscripts(self): - self.transcripts = {} - for fileset in self.cmdapp.testfiles: - for fname in glob.glob(fileset): - tfile = open(fname) - self.transcripts[fname] = iter(tfile.readlines()) - tfile.close() - if not len(self.transcripts): - raise Exception("No test files found - nothing to test.") - - def setUp(self): - if self.cmdapp: - self.fetchTranscripts() - - # Trap stdout - self._orig_stdout = self.cmdapp.stdout - self.cmdapp.stdout = OutputTrap() - - def runTest(self): # was testall - if self.cmdapp: - its = sorted(self.transcripts.items()) - for (fname, transcript) in its: - self._test_transcript(fname, transcript) - - def _test_transcript(self, fname, transcript): - line_num = 0 - finished = False - line = utils.strip_ansi(next(transcript)) - line_num += 1 - while not finished: - # Scroll forward to where actual commands begin - while not line.startswith(self.cmdapp.visible_prompt): - try: - line = utils.strip_ansi(next(transcript)) - except StopIteration: - finished = True - break - line_num += 1 - command = [line[len(self.cmdapp.visible_prompt):]] - line = next(transcript) - # Read the entirety of a multi-line command - while line.startswith(self.cmdapp.continuation_prompt): - command.append(line[len(self.cmdapp.continuation_prompt):]) - try: - line = next(transcript) - except StopIteration: - raise (StopIteration, - 'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num, - command[0]) - ) - line_num += 1 - command = ''.join(command) - # Send the command into the application and capture the resulting output - # TODO: Should we get the return value and act if stop == True? - self.cmdapp.onecmd_plus_hooks(command) - result = self.cmdapp.stdout.read() - # Read the expected result from transcript - if utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt): - message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format( - fname, line_num, command, result) - self.assert_(not (result.strip()), message) - continue - expected = [] - while not utils.strip_ansi(line).startswith(self.cmdapp.visible_prompt): - expected.append(line) - try: - line = next(transcript) - except StopIteration: - finished = True - break - line_num += 1 - expected = ''.join(expected) - - # transform the expected text into a valid regular expression - expected = self._transform_transcript_expected(expected) - message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format( - fname, line_num, command, expected, result) - self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message) - - def _transform_transcript_expected(self, s): - """parse the string with slashed regexes into a valid regex - - Given a string like: - - Match a 10 digit phone number: /\d{3}-\d{3}-\d{4}/ - - Turn it into a valid regular expression which matches the literal text - of the string and the regular expression. We have to remove the slashes - because they differentiate between plain text and a regular expression. - Unless the slashes are escaped, in which case they are interpreted as - plain text, or there is only one slash, which is treated as plain text - also. - - Check the tests in tests/test_transcript.py to see all the edge - cases. - """ - regex = '' - start = 0 - - while True: - (regex, first_slash_pos, start) = self._escaped_find(regex, s, start, False) - if first_slash_pos == -1: - # no more slashes, add the rest of the string and bail - regex += re.escape(s[start:]) - break - else: - # there is a slash, add everything we have found so far - # add stuff before the first slash as plain text - regex += re.escape(s[start:first_slash_pos]) - start = first_slash_pos+1 - # and go find the next one - (regex, second_slash_pos, start) = self._escaped_find(regex, s, start, True) - if second_slash_pos > 0: - # add everything between the slashes (but not the slashes) - # as a regular expression - regex += s[start:second_slash_pos] - # and change where we start looking for slashed on the - # turn through the loop - start = second_slash_pos + 1 - else: - # No closing slash, we have to add the first slash, - # and the rest of the text - regex += re.escape(s[start-1:]) - break - return regex - - @staticmethod - def _escaped_find(regex, s, start, in_regex): - """ - Find the next slash in {s} after {start} that is not preceded by a backslash. - - If we find an escaped slash, add everything up to and including it to regex, - updating {start}. {start} therefore serves two purposes, tells us where to start - looking for the next thing, and also tells us where in {s} we have already - added things to {regex} - - {in_regex} specifies whether we are currently searching in a regex, we behave - differently if we are or if we aren't. - """ - - while True: - pos = s.find('/', start) - if pos == -1: - # no match, return to caller - break - elif pos == 0: - # slash at the beginning of the string, so it can't be - # escaped. We found it. - break - else: - # check if the slash is preceeded by a backslash - if s[pos-1:pos] == '\\': - # it is. - if in_regex: - # add everything up to the backslash as a - # regular expression - regex += s[start:pos-1] - # skip the backslash, and add the slash - regex += s[pos] - else: - # add everything up to the backslash as escaped - # plain text - regex += re.escape(s[start:pos-1]) - # and then add the slash as escaped - # plain text - regex += re.escape(s[pos]) - # update start to show we have handled everything - # before it - start = pos+1 - # and continue to look - else: - # slash is not escaped, this is what we are looking for - break - return regex, pos, start - - def tearDown(self): - if self.cmdapp: - # Restore stdout - self.cmdapp.stdout = self._orig_stdout - - def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')): """Wrapper around namedtuple which lets you treat the last value as optional. -- cgit v1.2.1 From 6e49661f310bf88279c298326f236f0edd01791e Mon Sep 17 00:00:00 2001 From: kotfu Date: Sun, 6 May 2018 14:05:22 -0600 Subject: Defer import of InteractiveConsole --- cmd2/cmd2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 9428afb3..52dfc901 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -44,7 +44,6 @@ import sys import tempfile import traceback from typing import Callable, List, Optional, Union, Tuple -from code import InteractiveConsole import pyperclip @@ -2794,6 +2793,7 @@ Usage: Usage: unalias [-a] name [name ...] self.pystate['self'] = self localvars = self.pystate + from code import InteractiveConsole interp = InteractiveConsole(locals=localvars) interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())') -- cgit v1.2.1 From a479fa94516a9a93b8b58fd8c29479a40f6719d2 Mon Sep 17 00:00:00 2001 From: kotfu Date: Sun, 6 May 2018 14:17:21 -0600 Subject: Defer five imports - atexit - codecs - signal - tempfile - copy --- cmd2/cmd2.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 52dfc901..0a89b146 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -24,11 +24,8 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2 """ # many imports are lazy-loaded when they are needed import argparse -import atexit import cmd -import codecs import collections -import copy import datetime import functools import glob @@ -38,10 +35,8 @@ import os import platform import re import shlex -import signal import subprocess import sys -import tempfile import traceback from typing import Callable, List, Optional, Union, Tuple @@ -524,6 +519,7 @@ class AddSubmenu(object): self._copy_out_shared_attrs(_self, original_attributes) # Pass the submenu's tab completion parameters back up to the menu that called complete() + import copy _self.allow_appended_space = submenu.allow_appended_space _self.allow_closing_quote = submenu.allow_closing_quote _self.display_matches = copy.copy(submenu.display_matches) @@ -661,6 +657,7 @@ class Cmd(cmd.Cmd): readline.set_history_length(persistent_history_length) except FileNotFoundError: pass + import atexit atexit.register(readline.write_history_file, persistent_history_file) # Call super class constructor @@ -935,6 +932,7 @@ class Cmd(cmd.Cmd): On Failure Both items are None """ + import copy unclosed_quote = '' quotes_to_try = copy.copy(constants.QUOTES) @@ -1677,6 +1675,7 @@ class Cmd(cmd.Cmd): # Since self.display_matches is empty, set it to self.completion_matches # before we alter them. That way the suggestions will reflect how we parsed # the token being completed and not how readline did. + import copy self.display_matches = copy.copy(self.completion_matches) # Check if we need to add an opening quote @@ -1848,7 +1847,7 @@ class Cmd(cmd.Cmd): def preloop(self): """"Hook method executed once when the cmdloop() method is called.""" - + import signal # Register a default SIGINT signal handler for Ctrl+C signal.signal(signal.SIGINT, self.sigint_handler) @@ -2083,6 +2082,7 @@ class Cmd(cmd.Cmd): # Re-raise the exception raise ex elif statement.output: + import tempfile if (not statement.output_to) and (not can_clip): raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable') self.kept_state = Statekeeper(self, ('stdout',)) @@ -2927,6 +2927,7 @@ a..b, a:b, a:, ..b items by indices (inclusive) if runme: self.onecmd_plus_hooks(runme) elif args.edit: + import tempfile fd, fname = tempfile.mkstemp(suffix='.txt', text=True) with os.fdopen(fd, 'w') as fobj: for command in history: @@ -3138,6 +3139,8 @@ Script should contain one command per line, just like command would be typed in 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 -- cgit v1.2.1 From d9ca07a9b6c4525a86fb6e654550b83d84d5ae3d Mon Sep 17 00:00:00 2001 From: kotfu Date: Sun, 6 May 2018 14:42:35 -0600 Subject: Defer 5 more imports - datetime - functools - io - subprocess - traceback --- cmd2/cmd2.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 0a89b146..4cf972fd 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -26,18 +26,12 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2 import argparse import cmd import collections -import datetime -import functools import glob -import io -from io import StringIO import os import platform import re import shlex -import subprocess import sys -import traceback from typing import Callable, List, Optional, Union, Tuple import pyperclip @@ -133,6 +127,7 @@ def categorize(func: Union[Callable, Iterable], category: str) -> None: 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() @@ -170,6 +165,8 @@ def with_argument_list(func: Callable) -> Callable: method. Default passes a string of whatever the user typed. With this decorator, the decorated method will receive a list of arguments parsed from user input using shlex.split().""" + import functools + @functools.wraps(func) def cmd_wrapper(self, cmdline): lexed_arglist = parse_quoted_string(cmdline) @@ -186,6 +183,8 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser) -> Calla :param argparser: argparse.ArgumentParser - given instance of ArgumentParser :return: function that gets passed parsed args and a list of unknown args """ + import functools + # noinspection PyProtectedMember def arg_decorator(func: Callable): @functools.wraps(func) @@ -226,6 +225,7 @@ def with_argparser(argparser: argparse.ArgumentParser) -> Callable: :param argparser: argparse.ArgumentParser - given instance of ArgumentParser :return: function that gets passed parsed args """ + import functools # noinspection PyProtectedMember def arg_decorator(func: Callable): @@ -803,6 +803,7 @@ class Cmd(cmd.Cmd): :return: """ if self.debug: + import traceback traceback.print_exc() if exception_type is None: @@ -834,6 +835,7 @@ class Cmd(cmd.Cmd): :param msg: str - message to print to current stdout - anything convertible to a str with '{}'.format() is OK :param end: str - string appended after the end of the message if not already present, default a newline """ + import subprocess if msg is not None and msg != '': try: msg_str = '{}'.format(msg) @@ -1541,6 +1543,8 @@ class Cmd(cmd.Cmd): :param text: str - the current word that user is typing :param state: int - non-negative integer """ + import functools + if state == 0: unclosed_quote = '' self.set_completion_defaults() @@ -1911,6 +1915,7 @@ class Cmd(cmd.Cmd): if not sys.platform.startswith('win'): # Fix those annoying problems that occur with terminal programs like "less" when you pipe to them if self.stdin.isatty(): + import subprocess proc = subprocess.Popen(shlex.split('stty sane')) proc.communicate() return stop @@ -1935,6 +1940,7 @@ class Cmd(cmd.Cmd): :param line: str - line of text read from input :return: bool - True if cmdloop() should exit, False otherwise """ + import datetime stop = False try: statement = self._complete_statement(line) @@ -2054,6 +2060,9 @@ class Cmd(cmd.Cmd): :param statement: Statement - a parsed statement from the user """ + import io + import subprocess + if statement.pipe_to: self.kept_state = Statekeeper(self, ('stdout',)) @@ -2505,6 +2514,8 @@ Usage: Usage: unalias [-a] name [name ...] def _print_topics(self, header, cmds, verbose): """Customized version of print_topics that can switch between verbose or traditional output""" + import io + if cmds: if not verbose: self.print_topics(header, cmds, 15, 80) @@ -2539,7 +2550,7 @@ Usage: Usage: unalias [-a] name [name ...] doc = getattr(self, self._func_named(command)).__doc__ else: # we found the help function - result = StringIO() + result = io.StringIO() # try to redirect system stdout with redirect_stdout(result): # save our internal stdout @@ -2707,6 +2718,7 @@ Usage: Usage: unalias [-a] name [name ...] Usage: shell [arguments]""" + import subprocess try: # Use non-POSIX parsing to keep the quotes around the tokens tokens = shlex.split(command, posix=False) @@ -2962,6 +2974,8 @@ a..b, a:b, a:, ..b items by indices (inclusive) """Generate a transcript file from a given history of commands.""" # Save the current echo state, and turn it off. We inject commands into the # output using a different mechanism + import io + saved_echo = self.echo self.echo = False -- cgit v1.2.1 From ccfdf0f92043b1a28b871335c50caf6399e99982 Mon Sep 17 00:00:00 2001 From: kotfu Date: Sun, 6 May 2018 16:03:32 -0600 Subject: =?UTF-8?q?Extract=20AddSubmenu()=20into=20it=E2=80=99s=20own=20mo?= =?UTF-8?q?dule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd2/cmd2.py | 261 ----------------------------------------------------------- 1 file changed, 261 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 4cf972fd..c90d7dab 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -312,267 +312,6 @@ class EmptyStatement(Exception): pass -def _pop_readline_history(clear_history: bool=True) -> List[str]: - """Returns a copy of readline's history and optionally clears it (default)""" - # noinspection PyArgumentList - history = [ - readline.get_history_item(i) - for i in range(1, 1 + readline.get_current_history_length()) - ] - if clear_history: - readline.clear_history() - return history - - -def _push_readline_history(history, clear_history=True): - """Restores readline's history and optionally clears it first (default)""" - if clear_history: - readline.clear_history() - for line in history: - readline.add_history(line) - - -def _complete_from_cmd(cmd_obj, text, line, begidx, endidx): - """Complete as though the user was typing inside cmd's cmdloop()""" - from itertools import takewhile - command_subcommand_params = line.split(None, 3) - - if len(command_subcommand_params) < (3 if text else 2): - n = len(command_subcommand_params[0]) - n += sum(1 for _ in takewhile(str.isspace, line[n:])) - return cmd_obj.completenames(text, line[n:], begidx - n, endidx - n) - - command, subcommand = command_subcommand_params[:2] - n = len(command) + sum(1 for _ in takewhile(str.isspace, line)) - cfun = getattr(cmd_obj, 'complete_' + subcommand, cmd_obj.complete) - return cfun(text, line[n:], begidx - n, endidx - n) - - -class AddSubmenu(object): - """Conveniently add a submenu (Cmd-like class) to a Cmd - - e.g. given "class SubMenu(Cmd): ..." then - - @AddSubmenu(SubMenu(), 'sub') - class MyCmd(cmd.Cmd): - .... - - will have the following effects: - 1. 'sub' will interactively enter the cmdloop of a SubMenu instance - 2. 'sub cmd args' will call do_cmd(args) in a SubMenu instance - 3. 'sub ... [TAB]' will have the same behavior as [TAB] in a SubMenu cmdloop - i.e., autocompletion works the way you think it should - 4. 'help sub [cmd]' will print SubMenu's help (calls its do_help()) - """ - - class _Nonexistent(object): - """ - Used to mark missing attributes. - Disable __dict__ creation since this class does nothing - """ - __slots__ = () # - - def __init__(self, - submenu, - command, - aliases=(), - reformat_prompt="{super_prompt}>> {sub_prompt}", - shared_attributes=None, - require_predefined_shares=True, - create_subclass=False, - preserve_shares=False, - persistent_history_file=None - ): - """Set up the class decorator - - submenu (Cmd): Instance of something cmd.Cmd-like - - command (str): The command the user types to access the SubMenu instance - - aliases (iterable): More commands that will behave like "command" - - reformat_prompt (str): Format str or None to disable - if it's a string, it should contain one or more of: - {super_prompt}: The current cmd's prompt - {command}: The command in the current cmd with which it was called - {sub_prompt}: The subordinate cmd's original prompt - the default is "{super_prompt}{command} {sub_prompt}" - - shared_attributes (dict): dict of the form {'subordinate_attr': 'parent_attr'} - the attributes are copied to the submenu at the last moment; the submenu's - attributes are backed up before this and restored afterward - - require_predefined_shares: The shared attributes above must be independently - defined in the subordinate Cmd (default: True) - - create_subclass: put the modifications in a subclass rather than modifying - the existing class (default: False) - """ - self.submenu = submenu - self.command = command - self.aliases = aliases - if persistent_history_file: - self.persistent_history_file = os.path.expanduser(persistent_history_file) - else: - self.persistent_history_file = None - - if reformat_prompt is not None and not isinstance(reformat_prompt, str): - raise ValueError("reformat_prompt should be either a format string or None") - self.reformat_prompt = reformat_prompt - - self.shared_attributes = {} if shared_attributes is None else shared_attributes - if require_predefined_shares: - for attr in self.shared_attributes.keys(): - if not hasattr(submenu, attr): - raise AttributeError("The shared attribute '{attr}' is not defined in {cmd}. Either define {attr} " - "in {cmd} or set require_predefined_shares=False." - .format(cmd=submenu.__class__.__name__, attr=attr)) - - self.create_subclass = create_subclass - self.preserve_shares = preserve_shares - - def _get_original_attributes(self): - return { - attr: getattr(self.submenu, attr, AddSubmenu._Nonexistent) - for attr in self.shared_attributes.keys() - } - - def _copy_in_shared_attrs(self, parent_cmd): - for sub_attr, par_attr in self.shared_attributes.items(): - setattr(self.submenu, sub_attr, getattr(parent_cmd, par_attr)) - - def _copy_out_shared_attrs(self, parent_cmd, original_attributes): - if self.preserve_shares: - for sub_attr, par_attr in self.shared_attributes.items(): - setattr(parent_cmd, par_attr, getattr(self.submenu, sub_attr)) - else: - for attr, value in original_attributes.items(): - if attr is not AddSubmenu._Nonexistent: - setattr(self.submenu, attr, value) - else: - delattr(self.submenu, attr) - - def __call__(self, cmd_obj): - """Creates a subclass of Cmd wherein the given submenu can be accessed via the given command""" - def enter_submenu(parent_cmd, statement): - """ - This function will be bound to do_ and will change the scope of the CLI to that of the - submenu. - """ - submenu = self.submenu - original_attributes = self._get_original_attributes() - history = _pop_readline_history() - - if self.persistent_history_file: - try: - readline.read_history_file(self.persistent_history_file) - except FileNotFoundError: - pass - - try: - # copy over any shared attributes - self._copy_in_shared_attrs(parent_cmd) - - if statement.args: - # Remove the menu argument and execute the command in the submenu - submenu.onecmd_plus_hooks(statement.args) - else: - if self.reformat_prompt is not None: - prompt = submenu.prompt - submenu.prompt = self.reformat_prompt.format( - super_prompt=parent_cmd.prompt, - command=self.command, - sub_prompt=prompt, - ) - submenu.cmdloop() - if self.reformat_prompt is not None: - # noinspection PyUnboundLocalVariable - self.submenu.prompt = prompt - finally: - # copy back original attributes - self._copy_out_shared_attrs(parent_cmd, original_attributes) - - # write submenu history - if self.persistent_history_file: - readline.write_history_file(self.persistent_history_file) - # reset main app history before exit - _push_readline_history(history) - - def complete_submenu(_self, text, line, begidx, endidx): - """ - This function will be bound to complete_ and will perform the complete commands of the submenu. - """ - submenu = self.submenu - original_attributes = self._get_original_attributes() - try: - # copy over any shared attributes - self._copy_in_shared_attrs(_self) - - # Reset the submenu's tab completion parameters - submenu.allow_appended_space = True - submenu.allow_closing_quote = True - submenu.display_matches = [] - - return _complete_from_cmd(submenu, text, line, begidx, endidx) - finally: - # copy back original attributes - self._copy_out_shared_attrs(_self, original_attributes) - - # Pass the submenu's tab completion parameters back up to the menu that called complete() - import copy - _self.allow_appended_space = submenu.allow_appended_space - _self.allow_closing_quote = submenu.allow_closing_quote - _self.display_matches = copy.copy(submenu.display_matches) - - original_do_help = cmd_obj.do_help - original_complete_help = cmd_obj.complete_help - - def help_submenu(_self, line): - """ - This function will be bound to help_ and will call the help commands of the submenu. - """ - tokens = line.split(None, 1) - if tokens and (tokens[0] == self.command or tokens[0] in self.aliases): - self.submenu.do_help(tokens[1] if len(tokens) == 2 else '') - else: - original_do_help(_self, line) - - def _complete_submenu_help(_self, text, line, begidx, endidx): - """autocomplete to match help_submenu()'s behavior""" - tokens = line.split(None, 1) - if len(tokens) == 2 and ( - not (not tokens[1].startswith(self.command) and not any( - tokens[1].startswith(alias) for alias in self.aliases)) - ): - return self.submenu.complete_help( - text, - tokens[1], - begidx - line.index(tokens[1]), - endidx - line.index(tokens[1]), - ) - else: - return original_complete_help(_self, text, line, begidx, endidx) - - if self.create_subclass: - class _Cmd(cmd_obj): - do_help = help_submenu - complete_help = _complete_submenu_help - else: - _Cmd = cmd_obj - _Cmd.do_help = help_submenu - _Cmd.complete_help = _complete_submenu_help - - # Create bindings in the parent command to the submenus commands. - setattr(_Cmd, 'do_' + self.command, enter_submenu) - setattr(_Cmd, 'complete_' + self.command, complete_submenu) - - # Create additional bindings for aliases - for _alias in self.aliases: - setattr(_Cmd, 'do_' + _alias, enter_submenu) - setattr(_Cmd, 'complete_' + _alias, complete_submenu) - return _Cmd - - class Cmd(cmd.Cmd): """An easy but powerful framework for writing line-oriented command interpreters. -- cgit v1.2.1 From fc495a4201cee744807bee058115824287cd6b33 Mon Sep 17 00:00:00 2001 From: kotfu Date: Sun, 6 May 2018 17:55:30 -0600 Subject: Move more code from cmd2.py into utils.py --- cmd2/cmd2.py | 102 +++-------------------------------------------------------- 1 file changed, 4 insertions(+), 98 deletions(-) (limited to 'cmd2/cmd2.py') 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 - - -- cgit v1.2.1 From da0e211d40f89c36f81752ac0de3a971a86d8642 Mon Sep 17 00:00:00 2001 From: kotfu Date: Wed, 23 May 2018 22:27:29 -0600 Subject: Fix merge error --- cmd2/cmd2.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6a2f0e02..f4ed8d08 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1278,8 +1278,6 @@ class Cmd(cmd.Cmd): """ import functools if state == 0 and rl_type != RlType.NONE: - - if state == 0: unclosed_quote = '' self.set_completion_defaults() -- cgit v1.2.1 From 190fecb34ac91e25f64615f378d6d59ef6d77de8 Mon Sep 17 00:00:00 2001 From: kotfu Date: Thu, 24 May 2018 19:02:32 -0600 Subject: Make changes requested in PR #413 --- cmd2/cmd2.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f4ed8d08..f480b3ae 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -22,7 +22,13 @@ is used in place of `print`. Git repository on GitHub at https://github.com/python-cmd2/cmd2 """ -# many imports are lazy-loaded when they are needed +# This module has many imports, quite a few of which are only +# infrequently utilized. To reduce the initial overhead of +# import this module, many of these imports are lazy-loaded +# i.e. we only import the module when we use it +# For example, we don't import the 'traceback' module +# until the perror() function is called and the debug +# setting is True import argparse import cmd import collections @@ -33,7 +39,7 @@ import platform import re import shlex import sys -from typing import Callable, List, Optional, Union, Tuple +from typing import Callable, List, Union, Tuple import pyperclip -- cgit v1.2.1