cmd2 2.4.2__py3-none-any.whl → 2.5.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cmd2/history.py CHANGED
@@ -8,6 +8,9 @@ import re
8
8
  from collections import (
9
9
  OrderedDict,
10
10
  )
11
+ from dataclasses import (
12
+ dataclass,
13
+ )
11
14
  from typing import (
12
15
  Any,
13
16
  Callable,
@@ -19,17 +22,54 @@ from typing import (
19
22
  overload,
20
23
  )
21
24
 
22
- import attr
23
-
24
25
  from . import (
25
26
  utils,
26
27
  )
27
28
  from .parsing import (
28
29
  Statement,
30
+ shlex_split,
29
31
  )
30
32
 
31
33
 
32
- @attr.s(auto_attribs=True, frozen=True)
34
+ def single_line_format(statement: Statement) -> str:
35
+ """
36
+ Format a command line to display on a single line.
37
+
38
+ Spaces and newlines in quotes are preserved so those strings will span multiple lines.
39
+
40
+ :param statement: Statement being formatted.
41
+ :return: formatted command line
42
+ """
43
+ if not statement.raw:
44
+ return ""
45
+
46
+ lines = statement.raw.splitlines()
47
+ formatted_command = lines[0]
48
+
49
+ # Append any remaining lines to the command.
50
+ for line in lines[1:]:
51
+ try:
52
+ shlex_split(formatted_command)
53
+ except ValueError:
54
+ # We are in quotes, so restore the newline.
55
+ separator = "\n"
56
+ else:
57
+ # Don't add a space before line if one already exists or if it begins with the terminator.
58
+ if (
59
+ formatted_command.endswith(" ")
60
+ or line.startswith(" ")
61
+ or (statement.terminator and line.startswith(statement.terminator))
62
+ ):
63
+ separator = ""
64
+ else:
65
+ separator = " "
66
+
67
+ formatted_command += separator + line
68
+
69
+ return formatted_command
70
+
71
+
72
+ @dataclass(frozen=True)
33
73
  class HistoryItem:
34
74
  """Class used to represent one command in the history list"""
35
75
 
@@ -39,10 +79,10 @@ class HistoryItem:
39
79
  # Used in JSON dictionaries
40
80
  _statement_field = 'statement'
41
81
 
42
- statement: Statement = attr.ib(default=None, validator=attr.validators.instance_of(Statement))
82
+ statement: Statement
43
83
 
44
84
  def __str__(self) -> str:
45
- """A convenient human readable representation of the history item"""
85
+ """A convenient human-readable representation of the history item"""
46
86
  return self.statement.raw
47
87
 
48
88
  @property
@@ -84,15 +124,7 @@ class HistoryItem:
84
124
  if expanded:
85
125
  ret_str = self.expanded
86
126
  else:
87
- ret_str = self.raw.rstrip()
88
-
89
- # In non-verbose mode, display raw multiline commands on 1 line
90
- if self.statement.multiline_command:
91
- # This is an approximation and not meant to be a perfect piecing together of lines.
92
- # All newlines will be converted to spaces, including the ones in quoted strings that
93
- # are considered literals. Also if the final line starts with a terminator, then the
94
- # terminator will have an extra space before it in the 1 line version.
95
- ret_str = ret_str.replace('\n', ' ')
127
+ ret_str = single_line_format(self.statement).rstrip()
96
128
 
97
129
  # Display a numbered list if not writing to a script
98
130
  if not script:
@@ -118,13 +150,13 @@ class HistoryItem:
118
150
 
119
151
 
120
152
  class History(List[HistoryItem]):
121
- """A list of :class:`~cmd2.history.HistoryItem` objects with additional methods
153
+ """A list of [HistoryItem][cmd2.history.HistoryItem] objects with additional methods
122
154
  for searching and managing the list.
123
155
 
124
- :class:`~cmd2.Cmd` instantiates this class into the :data:`~cmd2.Cmd.history`
156
+ [cmd2.Cmd][] instantiates this class into the `cmd2.Cmd.history`
125
157
  attribute, and adds commands to it as a user enters them.
126
158
 
127
- See :ref:`features/history:History` for information about the built-in command
159
+ See [History](../features/history.md) for information about the built-in command
128
160
  which allows users to view, search, run, and save previously entered commands.
129
161
 
130
162
  Developers interested in accessing previously entered commands can use this
@@ -144,7 +176,6 @@ class History(List[HistoryItem]):
144
176
  """Start a new session, thereby setting the next index as the first index in the new session."""
145
177
  self.session_start_index = len(self)
146
178
 
147
- # noinspection PyMethodMayBeStatic
148
179
  def _zero_based_index(self, onebased: Union[int, str]) -> int:
149
180
  """Convert a one-based index to a zero-based index."""
150
181
  result = int(onebased)
@@ -153,12 +184,10 @@ class History(List[HistoryItem]):
153
184
  return result
154
185
 
155
186
  @overload
156
- def append(self, new: HistoryItem) -> None:
157
- ... # pragma: no cover
187
+ def append(self, new: HistoryItem) -> None: ... # pragma: no cover
158
188
 
159
189
  @overload
160
- def append(self, new: Statement) -> None:
161
- ... # pragma: no cover
190
+ def append(self, new: Statement) -> None: ... # pragma: no cover
162
191
 
163
192
  def append(self, new: Union[Statement, HistoryItem]) -> None:
164
193
  """Append a new statement to the end of the History list.
@@ -178,7 +207,7 @@ class History(List[HistoryItem]):
178
207
  """Get item from the History list using 1-based indexing.
179
208
 
180
209
  :param index: optional item to get
181
- :return: a single :class:`~cmd2.history.HistoryItem`
210
+ :return: a single [cmd2.history.HistoryItem][]
182
211
  """
183
212
  if index == 0:
184
213
  raise IndexError('The first command in history is command 1.')
cmd2/parsing.py CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  import re
6
6
  import shlex
7
+ from dataclasses import (
8
+ dataclass,
9
+ field,
10
+ )
7
11
  from typing import (
8
12
  Any,
9
13
  Dict,
@@ -14,8 +18,6 @@ from typing import (
14
18
  Union,
15
19
  )
16
20
 
17
- import attr
18
-
19
21
  from . import (
20
22
  constants,
21
23
  utils,
@@ -36,7 +38,7 @@ def shlex_split(str_to_split: str) -> List[str]:
36
38
  return shlex.split(str_to_split, comments=False, posix=False)
37
39
 
38
40
 
39
- @attr.s(auto_attribs=True, frozen=True)
41
+ @dataclass(frozen=True)
40
42
  class MacroArg:
41
43
  """
42
44
  Information used to replace or unescape arguments in a macro value when the macro is resolved
@@ -45,15 +47,15 @@ class MacroArg:
45
47
  """
46
48
 
47
49
  # The starting index of this argument in the macro value
48
- start_index: int = attr.ib(validator=attr.validators.instance_of(int))
50
+ start_index: int
49
51
 
50
52
  # The number string that appears between the braces
51
53
  # This is a string instead of an int because we support unicode digits and must be able
52
54
  # to reproduce this string later
53
- number_str: str = attr.ib(validator=attr.validators.instance_of(str))
55
+ number_str: str
54
56
 
55
57
  # Tells if this argument is escaped and therefore needs to be unescaped
56
- is_escaped: bool = attr.ib(validator=attr.validators.instance_of(bool))
58
+ is_escaped: bool
57
59
 
58
60
  # Pattern used to find normal argument
59
61
  # Digits surrounded by exactly 1 brace on a side and 1 or more braces on the opposite side
@@ -69,24 +71,24 @@ class MacroArg:
69
71
  digit_pattern = re.compile(r'\d+')
70
72
 
71
73
 
72
- @attr.s(auto_attribs=True, frozen=True)
74
+ @dataclass(frozen=True)
73
75
  class Macro:
74
76
  """Defines a cmd2 macro"""
75
77
 
76
78
  # Name of the macro
77
- name: str = attr.ib(validator=attr.validators.instance_of(str))
79
+ name: str
78
80
 
79
81
  # The string the macro resolves to
80
- value: str = attr.ib(validator=attr.validators.instance_of(str))
82
+ value: str
81
83
 
82
84
  # The minimum number of args the user has to pass to this macro
83
- minimum_arg_count: int = attr.ib(validator=attr.validators.instance_of(int))
85
+ minimum_arg_count: int
84
86
 
85
87
  # Used to fill in argument placeholders in the macro
86
- arg_list: List[MacroArg] = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list))
88
+ arg_list: List[MacroArg] = field(default_factory=list)
87
89
 
88
90
 
89
- @attr.s(auto_attribs=True, frozen=True)
91
+ @dataclass(frozen=True)
90
92
  class Statement(str): # type: ignore[override]
91
93
  """String subclass with additional attributes to store the results of parsing.
92
94
 
@@ -96,7 +98,7 @@ class Statement(str): # type: ignore[override]
96
98
  we add our own attributes to this subclass.
97
99
 
98
100
  Instances of this class should not be created by anything other than the
99
- :meth:`cmd2.parsing.StatementParser.parse` method, nor should any of the
101
+ [StatementParser.parse][cmd2.parsing.StatementParser.parse] method, nor should any of the
100
102
  attributes be modified once the object is created.
101
103
 
102
104
  The string portion of the class contains the arguments, but not the
@@ -106,46 +108,46 @@ class Statement(str): # type: ignore[override]
106
108
 
107
109
  1. `argparse <https://docs.python.org/3/library/argparse.html>`_ is your
108
110
  friend for anything complex. ``cmd2`` has the decorator
109
- (:func:`~cmd2.decorators.with_argparser`) which you can
111
+ ([cmd2.decorators.with_argparser][]) which you can
110
112
  use to make your command method receive a namespace of parsed arguments,
111
113
  whether positional or denoted with switches.
112
114
 
113
115
  2. For commands with simple positional arguments, use
114
- :attr:`~cmd2.Statement.args` or :attr:`~cmd2.Statement.arg_list`
116
+ [args][cmd2.Statement.args] or [arg_list][cmd2.Statement.arg_list]
115
117
 
116
118
  3. If you don't want to have to worry about quoted arguments, see
117
- :attr:`argv` for a trick which strips quotes off for you.
119
+ [argv][cmd2.Statement.argv] for a trick which strips quotes off for you.
118
120
  """
119
121
 
120
122
  # the arguments, but not the command, nor the output redirection clauses.
121
- args: str = attr.ib(default='', validator=attr.validators.instance_of(str))
123
+ args: str = ''
122
124
 
123
125
  # string containing exactly what we input by the user
124
- raw: str = attr.ib(default='', validator=attr.validators.instance_of(str))
126
+ raw: str = ''
125
127
 
126
128
  # the command, i.e. the first whitespace delimited word
127
- command: str = attr.ib(default='', validator=attr.validators.instance_of(str))
129
+ command: str = ''
128
130
 
129
131
  # list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted
130
- arg_list: List[str] = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list))
132
+ arg_list: List[str] = field(default_factory=list)
131
133
 
132
134
  # if the command is a multiline command, the name of the command, otherwise empty
133
- multiline_command: str = attr.ib(default='', validator=attr.validators.instance_of(str))
135
+ multiline_command: str = ''
134
136
 
135
137
  # the character which terminated the multiline command, if there was one
136
- terminator: str = attr.ib(default='', validator=attr.validators.instance_of(str))
138
+ terminator: str = ''
137
139
 
138
140
  # characters appearing after the terminator but before output redirection, if any
139
- suffix: str = attr.ib(default='', validator=attr.validators.instance_of(str))
141
+ suffix: str = ''
140
142
 
141
143
  # if output was piped to a shell command, the shell command as a string
142
- pipe_to: str = attr.ib(default='', validator=attr.validators.instance_of(str))
144
+ pipe_to: str = ''
143
145
 
144
146
  # if output was redirected, the redirection token, i.e. '>>'
145
- output: str = attr.ib(default='', validator=attr.validators.instance_of(str))
147
+ output: str = ''
146
148
 
147
149
  # if output was redirected, the destination file token (quotes preserved)
148
- output_to: str = attr.ib(default='', validator=attr.validators.instance_of(str))
150
+ output_to: str = ''
149
151
 
150
152
  # Used in JSON dictionaries
151
153
  _args_field = 'args'
@@ -156,7 +158,7 @@ class Statement(str): # type: ignore[override]
156
158
  We must override __new__ because we are subclassing `str` which is
157
159
  immutable and takes a different number of arguments as Statement.
158
160
 
159
- NOTE: attrs takes care of initializing other members in the __init__ it
161
+ NOTE: @dataclass takes care of initializing other members in the __init__ it
160
162
  generates.
161
163
  """
162
164
  stmt = super().__new__(cls, value)
@@ -200,8 +202,7 @@ class Statement(str): # type: ignore[override]
200
202
 
201
203
  @property
202
204
  def expanded_command_line(self) -> str:
203
- """Concatenate :meth:`~cmd2.Statement.command_and_args`
204
- and :meth:`~cmd2.Statement.post_command`"""
205
+ """Concatenate [command_and_args][cmd2.Statement.command_and_args] and [post_command][cmd2.Statement.post_command]"""
205
206
  return self.command_and_args + self.post_command
206
207
 
207
208
  @property
@@ -348,7 +349,7 @@ class StatementParser:
348
349
  return False, 'cannot start with the comment character'
349
350
 
350
351
  if not is_subcommand:
351
- for (shortcut, _) in self.shortcuts:
352
+ for shortcut, _ in self.shortcuts:
352
353
  if word.startswith(shortcut):
353
354
  # Build an error string with all shortcuts listed
354
355
  errmsg = 'cannot start with a shortcut: '
@@ -397,12 +398,12 @@ class StatementParser:
397
398
 
398
399
  def parse(self, line: str) -> Statement:
399
400
  """
400
- Tokenize the input and parse it into a :class:`~cmd2.Statement` object,
401
+ Tokenize the input and parse it into a [cmd2.parsing.Statement][] object,
401
402
  stripping comments, expanding aliases and shortcuts, and extracting output
402
403
  redirection directives.
403
404
 
404
405
  :param line: the command line being parsed
405
- :return: a new :class:`~cmd2.Statement` object
406
+ :return: a new [cmd2.parsing.Statement][] object
406
407
  :raises: Cmd2ShlexError if a shlex error occurs (e.g. No closing quotation)
407
408
  """
408
409
 
@@ -481,7 +482,6 @@ class StatementParser:
481
482
 
482
483
  # Check if output should be piped to a shell command
483
484
  if pipe_index < redir_index and pipe_index < append_index:
484
-
485
485
  # Get the tokens for the pipe command and expand ~ where needed
486
486
  pipe_to_tokens = tokens[pipe_index + 1 :]
487
487
  utils.expand_user_in_tokens(pipe_to_tokens)
@@ -543,7 +543,7 @@ class StatementParser:
543
543
  return statement
544
544
 
545
545
  def parse_command_only(self, rawinput: str) -> Statement:
546
- """Partially parse input into a :class:`~cmd2.Statement` object.
546
+ """Partially parse input into a [cmd2.Statement][] object.
547
547
 
548
548
  The command is identified, and shortcuts and aliases are expanded.
549
549
  Multiline commands are identified, but terminators and output
@@ -552,21 +552,21 @@ class StatementParser:
552
552
  This method is used by tab completion code and therefore must not
553
553
  generate an exception if there are unclosed quotes.
554
554
 
555
- The :class:`~cmd2.Statement` object returned by this method can at most
555
+ The [cmd2.parsing.Statement][] object returned by this method can at most
556
556
  contain values in the following attributes:
557
- :attr:`~cmd2.Statement.args`, :attr:`~cmd2.Statement.raw`,
558
- :attr:`~cmd2.Statement.command`,
559
- :attr:`~cmd2.Statement.multiline_command`
557
+ [cmd2.parsing.Statement.args][], [cmd2.parsing.Statement.raw][],
558
+ [cmd2.parsing.Statement.command][],
559
+ [cmd2.parsing.Statement.multiline_command][]
560
560
 
561
- :attr:`~cmd2.Statement.args` will include all output redirection
561
+ [cmd2.parsing.Statement.args][] will include all output redirection
562
562
  clauses and command terminators.
563
563
 
564
- Different from :meth:`~cmd2.parsing.StatementParser.parse` this method
564
+ Different from [cmd2.parsing.StatementParser.parse][] this method
565
565
  does not remove redundant whitespace within args. However, it does
566
566
  ensure args has no leading or trailing whitespace.
567
567
 
568
568
  :param rawinput: the command line as entered by the user
569
- :return: a new :class:`~cmd2.Statement` object
569
+ :return: a new [cmd2.Statement][] object
570
570
  """
571
571
  # expand shortcuts and aliases
572
572
  line = self._expand(rawinput)
@@ -580,8 +580,15 @@ class StatementParser:
580
580
 
581
581
  # take everything from the end of the first match group to
582
582
  # the end of the line as the arguments (stripping leading
583
- # and trailing spaces)
584
- args = line[match.end(1) :].strip()
583
+ # and unquoted trailing whitespace)
584
+ args = line[match.end(1) :].lstrip()
585
+ try:
586
+ shlex_split(args)
587
+ except ValueError:
588
+ # Unclosed quote. Leave trailing whitespace.
589
+ pass
590
+ else:
591
+ args = args.rstrip()
585
592
  # if the command is empty that means the input was either empty
586
593
  # or something weird like '>'. args should be empty if we couldn't
587
594
  # parse a command
@@ -609,18 +616,18 @@ class StatementParser:
609
616
  :param command_name: name of the command being run
610
617
  :param to_parse: what is being passed to the ``do_*`` method. It can be one of two types:
611
618
 
612
- 1. An already parsed :class:`~cmd2.Statement`
619
+ 1. An already parsed [cmd2.Statement][]
613
620
  2. An argument string in cases where a ``do_*`` method is
614
621
  explicitly called. Calling ``do_help('alias create')`` would
615
622
  cause ``to_parse`` to be 'alias create'.
616
623
 
617
624
  In this case, the string will be converted to a
618
- :class:`~cmd2.Statement` and returned along with
625
+ [cmd2.Statement][] and returned along with
619
626
  the argument list.
620
627
 
621
628
  :param preserve_quotes: if ``True``, then quotes will not be stripped from
622
629
  the arguments
623
- :return: A tuple containing the :class:`~cmd2.Statement` and a list of
630
+ :return: A tuple containing the [cmd2.Statement][] and a list of
624
631
  strings representing the arguments
625
632
  """
626
633
  # Check if to_parse needs to be converted to a Statement
@@ -656,7 +663,7 @@ class StatementParser:
656
663
  keep_expanding = bool(remaining_aliases)
657
664
 
658
665
  # expand shortcuts
659
- for (shortcut, expansion) in self.shortcuts:
666
+ for shortcut, expansion in self.shortcuts:
660
667
  if line.startswith(shortcut):
661
668
  # If the next character after the shortcut isn't a space, then insert one
662
669
  shortcut_len = len(shortcut)
@@ -701,7 +708,6 @@ class StatementParser:
701
708
  punctuated_tokens = []
702
709
 
703
710
  for cur_initial_token in tokens:
704
-
705
711
  # Save tokens up to 1 character in length or quoted tokens. No need to parse these.
706
712
  if len(cur_initial_token) <= 1 or cur_initial_token[0] in constants.QUOTES:
707
713
  punctuated_tokens.append(cur_initial_token)
@@ -716,7 +722,6 @@ class StatementParser:
716
722
 
717
723
  while True:
718
724
  if cur_char not in punctuation:
719
-
720
725
  # Keep appending to new_token until we hit a punctuation char
721
726
  while cur_char not in punctuation:
722
727
  new_token += cur_char
cmd2/plugin.py CHANGED
@@ -1,18 +1,20 @@
1
1
  #
2
2
  # coding=utf-8
3
3
  """Classes for the cmd2 plugin system"""
4
+
5
+ from dataclasses import (
6
+ dataclass,
7
+ )
4
8
  from typing import (
5
9
  Optional,
6
10
  )
7
11
 
8
- import attr
9
-
10
12
  from .parsing import (
11
13
  Statement,
12
14
  )
13
15
 
14
16
 
15
- @attr.s(auto_attribs=True)
17
+ @dataclass
16
18
  class PostparsingData:
17
19
  """Data class containing information passed to postparsing hook methods"""
18
20
 
@@ -20,14 +22,14 @@ class PostparsingData:
20
22
  statement: Statement
21
23
 
22
24
 
23
- @attr.s(auto_attribs=True)
25
+ @dataclass
24
26
  class PrecommandData:
25
27
  """Data class containing information passed to precommand hook methods"""
26
28
 
27
29
  statement: Statement
28
30
 
29
31
 
30
- @attr.s(auto_attribs=True)
32
+ @dataclass
31
33
  class PostcommandData:
32
34
  """Data class containing information passed to postcommand hook methods"""
33
35
 
@@ -35,7 +37,7 @@ class PostcommandData:
35
37
  statement: Statement
36
38
 
37
39
 
38
- @attr.s(auto_attribs=True)
40
+ @dataclass
39
41
  class CommandFinalizationData:
40
42
  """Data class containing information passed to command finalization hook methods"""
41
43
 
cmd2/py_bridge.py CHANGED
@@ -57,7 +57,7 @@ class CommandResult(NamedTuple):
57
57
  if isinstance(sys.stderr, StdSim):
58
58
  sys.stderr.pause_storage = True
59
59
 
60
- See :class:`~cmd2.utils.StdSim` for more information.
60
+ See [cmd2.utils.StdSim][] for more information.
61
61
 
62
62
  .. note::
63
63
 
@@ -83,10 +83,17 @@ class CommandResult(NamedTuple):
83
83
 
84
84
 
85
85
  class PyBridge:
86
- """Provides a Python API wrapper for application commands."""
86
+ """
87
+ Provides a Python API wrapper for application commands.
88
+
89
+ :param cmd2_app: app being controlled by this PyBridge.
90
+ :param add_to_history: If True, then add all commands run by this PyBridge to history.
91
+ Defaults to True.
92
+ """
87
93
 
88
- def __init__(self, cmd2_app: 'cmd2.Cmd') -> None:
94
+ def __init__(self, cmd2_app: 'cmd2.Cmd', *, add_to_history: bool = True) -> None:
89
95
  self._cmd2_app = cmd2_app
96
+ self._add_to_history = add_to_history
90
97
  self.cmd_echo = False
91
98
 
92
99
  # Tells if any of the commands run via __call__ returned True for stop
@@ -126,7 +133,11 @@ class PyBridge:
126
133
  self._cmd2_app.stdout = cast(TextIO, copy_cmd_stdout)
127
134
  with redirect_stdout(cast(IO[str], copy_cmd_stdout)):
128
135
  with redirect_stderr(cast(IO[str], copy_stderr)):
129
- stop = self._cmd2_app.onecmd_plus_hooks(command, py_bridge_call=True)
136
+ stop = self._cmd2_app.onecmd_plus_hooks(
137
+ command,
138
+ add_to_history=self._add_to_history,
139
+ py_bridge_call=True,
140
+ )
130
141
  finally:
131
142
  with self._cmd2_app.sigint_protection:
132
143
  self._cmd2_app.stdout = cast(IO[str], copy_cmd_stdout.inner_stream)