Skip to content

Commit 57dd827

Browse files
authored
Merge pull request #648 from python-cmd2/attributes
Converted class attributes to instance attributes
2 parents 83a2872 + 4523bc6 commit 57dd827

23 files changed

+190
-164
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## 0.9.12 (TBD, 2019)
1+
## 0.9.12 (March TBD, 2019)
22
* Enhancements
33
* Added ability to include command name placeholders in the message printed when trying to run a disabled command.
44
* See docstring for ``disable_command()`` or ``disable_category()`` for more details.
@@ -15,7 +15,11 @@
1515
* ``do_help()`` - when no help information can be found
1616
* ``default()`` - in all cases since this is called when an invalid command name is run
1717
* ``_report_disabled_command_usage()`` - in all cases since this is called when a disabled command is run
18-
* Removed *** from beginning of error messages printed by `do_help()` and `default()`.
18+
* Removed *** from beginning of error messages printed by `do_help()` and `default()`
19+
* Significantly refactored ``cmd.Cmd`` class so that all class attributes got converted to instance attributes, also:
20+
* Added ``allow_redirection``, ``terminators``, ``multiline_commands``, and ``shortcuts`` as optional arguments
21+
to ``cmd.Cmd.__init__()`
22+
* A few instance attributes were moved inside ``StatementParser`` and properties were created for accessing them
1923

2024
## 0.9.11 (March 13, 2019)
2125
* Bug Fixes

README.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,9 @@ Instructions for implementing each feature follow.
185185

186186
- Multi-line commands
187187

188-
Any command accepts multi-line input when its name is listed in `Cmd.multiline_commands`.
189-
The program will keep expecting input until a line ends with any of the characters
190-
in `Cmd.terminators` . The default terminators are `;` and `/n` (empty newline).
188+
Any command accepts multi-line input when its name is listed the `multiline_commands` optional argument to
189+
`cmd2.Cmd.__init`. The program will keep expecting input until a line ends with any of the characters listed in the
190+
`terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `/n` (empty newline).
191191

192192
- Special-character shortcut commands (beyond cmd's "@" and "!")
193193

@@ -239,14 +239,12 @@ class CmdLineApp(cmd2.Cmd):
239239
MUMBLE_LAST = ['right?']
240240

241241
def __init__(self):
242-
self.multiline_commands = ['orate']
243242
self.maxrepeats = 3
244-
245-
# Add stuff to shortcuts before calling base class initializer
246-
self.shortcuts.update({'&': 'speak'})
243+
shortcuts = dict(self.DEFAULT_SHORTCUTS)
244+
shortcuts.update({'&': 'speak'})
247245

248246
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
249-
super().__init__(use_ipython=False)
247+
super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
250248

251249
# Make maxrepeats settable at runtime
252250
self.settable['maxrepeats'] = 'max repetitions for speak command'

cmd2/clipboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
try:
1111
from pyperclip.exceptions import PyperclipException
1212
except ImportError: # pragma: no cover
13-
# noinspection PyUnresolvedReferences
13+
# noinspection PyUnresolvedReferences,PyProtectedMember
1414
from pyperclip import PyperclipException
1515

1616
# Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux

cmd2/cmd2.py

Lines changed: 77 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
if rl_type == RlType.PYREADLINE:
6969

7070
# Save the original pyreadline display completion function since we need to override it and restore it
71-
# noinspection PyProtectedMember
71+
# noinspection PyProtectedMember,PyUnresolvedReferences
7272
orig_pyreadline_display = readline.rl.mode._display_completions
7373

7474
elif rl_type == RlType.GNU:
@@ -104,6 +104,7 @@ def __subclasshook__(cls, C):
104104

105105
# Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout
106106
if sys.version_info < (3, 5):
107+
# noinspection PyUnresolvedReferences
107108
from contextlib2 import redirect_stdout
108109
else:
109110
from contextlib import redirect_stdout
@@ -310,52 +311,14 @@ class Cmd(cmd.Cmd):
310311
311312
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
312313
"""
313-
# Attributes used to configure the StatementParser, best not to change these at runtime
314-
multiline_commands = []
315-
shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
316-
terminators = [constants.MULTILINE_TERMINATOR]
317-
318-
# Attributes which are NOT dynamically settable at runtime
319-
allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
320-
allow_redirection = True # Should output redirection and pipes be allowed
321-
default_to_shell = False # Attempt to run unrecognized commands as shell commands
322-
quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt
323-
reserved_words = []
324-
325-
# Attributes which ARE dynamically settable at runtime
326-
colors = constants.COLORS_TERMINAL
327-
continuation_prompt = '> '
328-
debug = False
329-
echo = False
330-
editor = os.environ.get('EDITOR')
331-
if not editor:
332-
if sys.platform[:3] == 'win':
333-
editor = 'notepad'
334-
else:
335-
# Favor command-line editors first so we don't leave the terminal to edit
336-
for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']:
337-
if utils.which(editor):
338-
break
339-
feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
340-
locals_in_py = False
341-
quiet = False # Do not suppress nonessential output
342-
timing = False # Prints elapsed time for each command
343-
344-
# To make an attribute settable with the "do_set" command, add it to this ...
345-
settable = {'colors': 'Allow colorized output (valid values: Terminal, Always, Never)',
346-
'continuation_prompt': 'On 2nd+ line of input',
347-
'debug': 'Show full error stack on error',
348-
'echo': 'Echo command issued into output',
349-
'editor': 'Program used by ``edit``',
350-
'feedback_to_output': 'Include nonessentials in `|`, `>` results',
351-
'locals_in_py': 'Allow access to your application in py via self',
352-
'prompt': 'The prompt issued to solicit input',
353-
'quiet': "Don't print nonessential feedback",
354-
'timing': 'Report execution times'}
314+
DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'}
315+
DEFAULT_EDITOR = utils.find_editor()
355316

356317
def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent_history_file: str = '',
357318
persistent_history_length: int = 1000, startup_script: Optional[str] = None, use_ipython: bool = False,
358-
transcript_files: Optional[List[str]] = None) -> None:
319+
transcript_files: Optional[List[str]] = None, allow_redirection: bool = True,
320+
multiline_commands: Optional[List[str]] = None, terminators: Optional[List[str]] = None,
321+
shortcuts: Optional[Dict[str, str]] = None) -> None:
359322
"""An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package.
360323
361324
:param completekey: (optional) readline name of a completion key, default to Tab
@@ -366,6 +329,9 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
366329
:param startup_script: (optional) file path to a a script to load and execute at startup
367330
:param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell
368331
:param transcript_files: (optional) allows running transcript tests when allow_cli_args is False
332+
:param allow_redirection: (optional) should output redirection and pipes be allowed
333+
:param multiline_commands: (optional) list of commands allowed to accept multi-line input
334+
:param shortcuts: (optional) dictionary containing shortcuts for commands
369335
"""
370336
# If use_ipython is False, make sure the do_ipy() method doesn't exit
371337
if not use_ipython:
@@ -384,31 +350,56 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
384350
# Call super class constructor
385351
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
386352

353+
# Attributes which should NOT be dynamically settable at runtime
354+
self.allow_cli_args = True # Should arguments passed on the command-line be processed as commands?
355+
self.default_to_shell = False # Attempt to run unrecognized commands as shell commands
356+
self.quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt
357+
358+
# Attributes which ARE dynamically settable at runtime
359+
self.colors = constants.COLORS_TERMINAL
360+
self.continuation_prompt = '> '
361+
self.debug = False
362+
self.echo = False
363+
self.editor = self.DEFAULT_EDITOR
364+
self.feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing)
365+
self.locals_in_py = False
366+
self.quiet = False # Do not suppress nonessential output
367+
self.timing = False # Prints elapsed time for each command
368+
369+
# To make an attribute settable with the "do_set" command, add it to this ...
370+
self.settable = {'colors': 'Allow colorized output (valid values: Terminal, Always, Never)',
371+
'continuation_prompt': 'On 2nd+ line of input',
372+
'debug': 'Show full error stack on error',
373+
'echo': 'Echo command issued into output',
374+
'editor': 'Program used by ``edit``',
375+
'feedback_to_output': 'Include nonessentials in `|`, `>` results',
376+
'locals_in_py': 'Allow access to your application in py via self',
377+
'prompt': 'The prompt issued to solicit input',
378+
'quiet': "Don't print nonessential feedback",
379+
'timing': 'Report execution times'}
380+
387381
# Commands to exclude from the help menu and tab completion
388382
self.hidden_commands = ['eof', 'eos', '_relative_load']
389383

390384
# Commands to exclude from the history command
391385
self.exclude_from_history = '''history edit eof eos'''.split()
392386

393387
# Command aliases and macros
394-
self.aliases = dict()
395388
self.macros = dict()
396389

397-
self._finalize_app_parameters()
398-
399390
self.initial_stdout = sys.stdout
400391
self.history = History()
401392
self.pystate = {}
402393
self.py_history = []
403394
self.pyscript_name = 'app'
404-
self.keywords = self.reserved_words + self.get_all_commands()
405-
self.statement_parser = StatementParser(
406-
allow_redirection=self.allow_redirection,
407-
terminators=self.terminators,
408-
multiline_commands=self.multiline_commands,
409-
aliases=self.aliases,
410-
shortcuts=self.shortcuts,
411-
)
395+
396+
if shortcuts is None:
397+
shortcuts = self.DEFAULT_SHORTCUTS
398+
shortcuts = sorted(shortcuts.items(), reverse=True)
399+
self.statement_parser = StatementParser(allow_redirection=allow_redirection,
400+
terminators=terminators,
401+
multiline_commands=multiline_commands,
402+
shortcuts=shortcuts)
412403
self._transcript_files = transcript_files
413404

414405
# Used to enable the ability for a Python script to quit the application
@@ -568,10 +559,25 @@ def visible_prompt(self) -> str:
568559
"""
569560
return utils.strip_ansi(self.prompt)
570561

571-
def _finalize_app_parameters(self) -> None:
572-
"""Finalize the shortcuts"""
573-
# noinspection PyUnresolvedReferences
574-
self.shortcuts = sorted(self.shortcuts.items(), reverse=True)
562+
@property
563+
def aliases(self) -> Dict[str, str]:
564+
"""Read-only property to access the aliases stored in the StatementParser."""
565+
return self.statement_parser.aliases
566+
567+
@property
568+
def shortcuts(self) -> Tuple[Tuple[str, str]]:
569+
"""Read-only property to access the shortcuts stored in the StatementParser."""
570+
return self.statement_parser.shortcuts
571+
572+
@property
573+
def allow_redirection(self) -> bool:
574+
"""Getter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
575+
return self.statement_parser.allow_redirection
576+
577+
@allow_redirection.setter
578+
def allow_redirection(self, value: bool) -> None:
579+
"""Setter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
580+
self.statement_parser.allow_redirection = value
575581

576582
def decolorized_write(self, fileobj: IO, msg: str) -> None:
577583
"""Write a string to a fileobject, stripping ANSI escape sequences if necessary
@@ -728,6 +734,7 @@ def reset_completion_defaults(self) -> None:
728734
if rl_type == RlType.GNU:
729735
readline.set_completion_display_matches_hook(self._display_matches_gnu_readline)
730736
elif rl_type == RlType.PYREADLINE:
737+
# noinspection PyUnresolvedReferences
731738
readline.rl.mode._display_completions = self._display_matches_pyreadline
732739

733740
def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[List[str], List[str]]:
@@ -1355,6 +1362,7 @@ def _display_matches_pyreadline(self, matches: List[str]) -> None: # pragma: no
13551362

13561363
# Print the header if one exists
13571364
if self.completion_header:
1365+
# noinspection PyUnresolvedReferences
13581366
readline.rl.mode.console.write('\n' + self.completion_header)
13591367

13601368
# Display matches using actual display function. This also redraws the prompt and line.
@@ -2203,6 +2211,7 @@ def _cmdloop(self) -> bool:
22032211
readline.set_completion_display_matches_hook(None)
22042212
rl_basic_quote_characters.value = old_basic_quotes
22052213
elif rl_type == RlType.PYREADLINE:
2214+
# noinspection PyUnresolvedReferences
22062215
readline.rl.mode._display_completions = orig_pyreadline_display
22072216

22082217
self.cmdqueue.clear()
@@ -2822,7 +2831,8 @@ def cmdenvironment(self) -> str:
28222831
Commands may be terminated with: {}
28232832
Arguments at invocation allowed: {}
28242833
Output redirection and pipes allowed: {}"""
2825-
return read_only_settings.format(str(self.terminators), self.allow_cli_args, self.allow_redirection)
2834+
return read_only_settings.format(str(self.statement_parser.terminators), self.allow_cli_args,
2835+
self.allow_redirection)
28262836

28272837
def show(self, args: argparse.Namespace, parameter: str = '') -> None:
28282838
"""Shows current settings of parameters.
@@ -3047,6 +3057,7 @@ def py_quit():
30473057
# Save cmd2 history
30483058
saved_cmd2_history = []
30493059
for i in range(1, readline.get_current_history_length() + 1):
3060+
# noinspection PyArgumentList
30503061
saved_cmd2_history.append(readline.get_history_item(i))
30513062

30523063
readline.clear_history()
@@ -3079,6 +3090,7 @@ def py_quit():
30793090
if rl_type == RlType.GNU:
30803091
readline.set_completion_display_matches_hook(None)
30813092
elif rl_type == RlType.PYREADLINE:
3093+
# noinspection PyUnresolvedReferences
30823094
readline.rl.mode._display_completions = self._display_matches_pyreadline
30833095

30843096
# Save off the current completer and set a new one in the Python console
@@ -3116,6 +3128,7 @@ def py_quit():
31163128
# Save py's history
31173129
self.py_history.clear()
31183130
for i in range(1, readline.get_current_history_length() + 1):
3131+
# noinspection PyArgumentList
31193132
self.py_history.append(readline.get_history_item(i))
31203133

31213134
readline.clear_history()
@@ -3193,10 +3206,12 @@ def do_ipy(self, _: argparse.Namespace) -> None:
31933206
exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
31943207

31953208
if self.locals_in_py:
3196-
def load_ipy(self, app):
3209+
# noinspection PyUnusedLocal
3210+
def load_ipy(cmd2_instance, app):
31973211
embed(banner1=banner, exit_msg=exit_msg)
31983212
load_ipy(self, bridge)
31993213
else:
3214+
# noinspection PyUnusedLocal
32003215
def load_ipy(app):
32013216
embed(banner1=banner, exit_msg=exit_msg)
32023217
load_ipy(bridge)
@@ -3598,6 +3613,7 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
35983613
if rl_type == RlType.GNU:
35993614
sys.stderr.write(terminal_str)
36003615
elif rl_type == RlType.PYREADLINE:
3616+
# noinspection PyUnresolvedReferences
36013617
readline.rl.mode.console.write(terminal_str)
36023618

36033619
# Redraw the prompt and input lines

cmd2/history.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ def get(self, index: Union[int, str]) -> HistoryItem:
128128
# \s*$ match any whitespace at the end of the input. This is here so
129129
# you don't have to trim the input
130130
#
131-
spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]{1}\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]{1}\d*)?\s*$')
131+
spanpattern = re.compile(r'^\s*(?P<start>-?[1-9]\d*)?(?P<separator>:|(\.{2,}))?(?P<end>-?[1-9]\d*)?\s*$')
132132

133133
def span(self, span: str) -> List[HistoryItem]:
134134
"""Return an index or slice of the History list,
135135
136-
:param raw: string containing an index or a slice
136+
:param span: string containing an index or a slice
137137
:return: a list of HistoryItems
138138
139139
This method can accommodate input in any of these forms:

0 commit comments

Comments
 (0)