cli-command-parser 2023.4.10__tar.gz → 2023.4.16__tar.gz

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.
Files changed (61) hide show
  1. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/PKG-INFO +18 -1
  2. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/__init__.py +8 -1
  3. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/__version__.py +1 -1
  4. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/command_parameters.py +46 -32
  5. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/commands.py +2 -2
  6. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/config.py +31 -0
  7. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/context.py +8 -25
  8. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/command_builder.py +24 -6
  9. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/visitor.py +16 -14
  10. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/core.py +9 -6
  11. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/commands.py +2 -2
  12. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/params.py +7 -8
  13. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/metadata.py +94 -19
  14. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/nargs.py +37 -14
  15. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/base.py +39 -21
  16. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/choice_map.py +24 -17
  17. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/groups.py +5 -1
  18. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/options.py +17 -8
  19. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/pass_thru.py +1 -1
  20. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/positionals.py +8 -1
  21. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parse_tree.py +16 -21
  22. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parser.py +80 -75
  23. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/typing.py +2 -1
  24. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/utils.py +2 -3
  25. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/PKG-INFO +18 -1
  26. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/requires.txt +3 -0
  27. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/readme.rst +17 -0
  28. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/setup.cfg +2 -0
  29. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/LICENSE +0 -0
  30. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/MANIFEST.in +0 -0
  31. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/__main__.py +0 -0
  32. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/actions.py +0 -0
  33. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/annotations.py +0 -0
  34. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/compat.py +0 -0
  35. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/__init__.py +0 -0
  36. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/__main__.py +0 -0
  37. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
  38. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  39. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/utils.py +0 -0
  40. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/documentation.py +0 -0
  41. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/error_handling.py +0 -0
  42. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/exceptions.py +0 -0
  43. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/__init__.py +0 -0
  44. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
  45. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/utils.py +0 -0
  46. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/__init__.py +0 -0
  47. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/base.py +0 -0
  48. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/choices.py +0 -0
  49. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/exceptions.py +0 -0
  50. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/files.py +0 -0
  51. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/numeric.py +0 -0
  52. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/time.py +0 -0
  53. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/utils.py +0 -0
  54. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/__init__.py +0 -0
  55. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/option_strings.py +0 -0
  56. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/testing.py +0 -0
  57. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -0
  58. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  59. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  60. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/pyproject.toml +0 -0
  61. {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/requirements-dev.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cli_command_parser
3
- Version: 2023.4.10
3
+ Version: 2023.4.16
4
4
  Summary: CLI Command Parser
5
5
  Home-page: https://github.com/dskrypa/cli_command_parser
6
6
  Author: Doug Skrypa
@@ -111,6 +111,23 @@ with optional dependencies::
111
111
  $ pip install -U cli-command-parser[wcwidth]
112
112
 
113
113
 
114
+ Python Version Compatibility
115
+ ============================
116
+
117
+ Python versions 3.7 and above are currently supported. CLI Command Parser will no longer support 3.7 after 2023-04-30,
118
+ ahead of the `official end of support for 3.7 on 2023-06-27 <https://devguide.python.org/versions/>`__.
119
+
120
+ When using 3.7 or 3.8, some additional packages that backport functionality that was added in later Python versions
121
+ are required for compatibility.
122
+
123
+ To use the argparse to cli-command-parser conversion script with Python 3.7 or 3.8, there is a dependency on
124
+ `astunparse <https://astunparse.readthedocs.io>`__. If you are using Python 3.9 or above, then ``astunparse`` is not
125
+ necessary because the relevant code was added to the stdlib ``ast`` module. If you're unsure, you can install
126
+ cli-command-parser with the following command to automatically handle whether that extra dependency is needed or not::
127
+
128
+ $ pip install -U cli-command-parser[conversion]
129
+
130
+
114
131
  Links
115
132
  *****
116
133
 
@@ -4,7 +4,14 @@ Command Parser
4
4
  :author: Doug Skrypa
5
5
  """
6
6
 
7
- from .config import CommandConfig, ShowDefaults, OptionNameMode, SubcommandAliasHelpMode
7
+ from .config import (
8
+ CommandConfig,
9
+ ShowDefaults,
10
+ OptionNameMode,
11
+ SubcommandAliasHelpMode,
12
+ AmbiguousComboMode,
13
+ AllowLeadingDash,
14
+ )
8
15
  from .commands import Command, main
9
16
  from .context import Context, get_current_context, ctx, get_parsed, get_context, get_raw_arg
10
17
  from .exceptions import (
@@ -1,7 +1,7 @@
1
1
  __title__ = 'cli_command_parser'
2
2
  __description__ = 'CLI Command Parser'
3
3
  __url__ = 'https://github.com/dskrypa/cli_command_parser'
4
- __version__ = '2023.04.10'
4
+ __version__ = '2023.04.16'
5
5
  __author__ = 'Doug Skrypa'
6
6
  __author_email__ = 'dskrypa@gmail.com'
7
7
  __license__ = 'Apache 2.0'
@@ -50,6 +50,7 @@ class CommandParameters:
50
50
  combo_option_map: OptionMap #: Mapping of {short opt: Parameter} (no dash characters)
51
51
  groups: List[ParamGroup] #: List of ParamGroup objects
52
52
  positionals: List[BasePositional] #: List of positional Parameters
53
+ _deferred_positionals: List[BasePositional] = () #: Positional Parameters that are deferred to sub commands
53
54
  option_map: OptionMap #: Mapping of {--opt / -opt: Parameter}
54
55
 
55
56
  def __init__(self, command: CommandCls, command_parent: Optional[CommandCls], config: CommandConfig):
@@ -75,6 +76,15 @@ class CommandParameters:
75
76
  return self.parent.pass_thru
76
77
  return None
77
78
 
79
+ @cached_property
80
+ def all_positionals(self) -> List[BasePositional]:
81
+ try:
82
+ if not self.parent.sub_command:
83
+ return self.parent.all_positionals + self.positionals
84
+ except AttributeError:
85
+ pass
86
+ return self.positionals
87
+
78
88
  @cached_property
79
89
  def always_available_action_flags(self) -> Tuple[ActionFlag, ...]:
80
90
  """
@@ -92,8 +102,8 @@ class CommandParameters:
92
102
  else:
93
103
  formatter_factory = self.config.command_formatter or CommandHelpFormatter
94
104
  formatter = formatter_factory(self.command, self)
105
+ formatter.maybe_add_positionals(self.all_positionals)
95
106
  formatter.maybe_add_option(self._pass_thru)
96
- formatter.maybe_add_positionals(self.positionals)
97
107
  formatter.maybe_add_options(self.options)
98
108
  formatter.maybe_add_groups(self.groups)
99
109
  return formatter
@@ -129,36 +139,38 @@ class CommandParameters:
129
139
 
130
140
  # region Initialization
131
141
 
132
- def _process_parameters(self):
133
- """
134
- Process all of the :class:`.Parameter` / :class:`.ParamGroup` members in the associated :class:`.Command` class.
135
- """
142
+ def _iter_parameters(self) -> Iterator[ParamBase]:
136
143
  name_param_map = {} # Allow subclasses to override names, but not within a given command
137
- positionals = []
138
- options = []
139
- groups = set()
140
-
141
144
  for attr, param in self.command.__dict__.items():
142
145
  if attr.startswith('__') or not isinstance(param, ParamBase): # Name mangled Parameters are still processed
143
146
  continue
144
-
145
- name = param.name
146
147
  try:
147
- other_attr, other_param = name_param_map[name]
148
+ other_attr, other_param = name_param_map[param.name]
148
149
  except KeyError:
149
- name_param_map[name] = (attr, param)
150
+ name_param_map[param.name] = (attr, param)
151
+ yield param
150
152
  else:
151
153
  raise CommandDefinitionError(
152
154
  'Name conflict - multiple parameters within a Command cannot have the same name - conflicting'
153
155
  f' params: {other_attr}={other_param}, {attr}={param}'
154
156
  )
155
157
 
158
+ def _process_parameters(self):
159
+ """
160
+ Process all of the :class:`.Parameter` / :class:`.ParamGroup` members in the associated :class:`.Command` class.
161
+ """
162
+ positionals = []
163
+ options = []
164
+ groups = set()
165
+
166
+ for param in self._iter_parameters():
156
167
  if isinstance(param, BasePositional):
157
168
  positionals.append(param)
158
169
  elif isinstance(param, BaseOption):
159
170
  options.append(param)
160
171
  elif isinstance(param, ParamGroup):
161
172
  # Groups will only be discovered here when defined with `as` - ex: `with ParamGroup(...) as foo:`
173
+ # Group members will always be discovered at the top level since context managers share the outer scope
162
174
  groups.add(param)
163
175
  elif isinstance(param, PassThru):
164
176
  if self.pass_thru:
@@ -190,36 +202,37 @@ class CommandParameters:
190
202
  self.groups = sorted(groups)
191
203
 
192
204
  def _process_positionals(self, params: List[BasePositional]):
193
- var_nargs_param = None
194
- for param in params:
195
- if self.sub_command:
196
- raise CommandDefinitionError(
197
- f'Positional param={param!r} may not follow the sub command {self.sub_command} - re-order the'
198
- ' positionals, move it into the sub command(s), or convert it to an optional parameter'
199
- )
200
- elif var_nargs_param:
205
+ var_nargs_param = action_or_sub_cmd = split_index = None
206
+ for i, param in enumerate(params):
207
+ if var_nargs_param:
201
208
  raise CommandDefinitionError(
202
209
  f'Additional Positional parameters cannot follow {var_nargs_param} because it accepts'
203
210
  f' a variable number of arguments with no specific choices defined - param={param!r} is invalid'
204
211
  )
205
-
206
- if isinstance(param, (SubCommand, Action)):
207
- if self.action: # self.sub_command being already defined is handled above
212
+ elif isinstance(param, (SubCommand, Action)):
213
+ if action_or_sub_cmd:
208
214
  raise CommandDefinitionError(
209
215
  f'Only 1 Action xor SubCommand is allowed in a given Command - {self.command.__name__} cannot'
210
- f' contain both {self.action} and {param}'
216
+ f' contain both {action_or_sub_cmd} and {param}'
211
217
  )
212
218
  elif isinstance(param, SubCommand):
213
- self.sub_command = param
219
+ self.sub_command = action_or_sub_cmd = param
220
+ split_index = i + 1
214
221
  else:
215
- self.action = param
222
+ self.action = action_or_sub_cmd = param
216
223
  if not param.has_choices:
217
224
  raise CommandDefinitionError(f'No choices were registered for {self.action}')
218
-
219
- if param.nargs.variable and not param.has_choices:
225
+ elif param.nargs.variable and not param.has_choices:
220
226
  var_nargs_param = param
221
227
 
222
- self.positionals = params
228
+ if split_index:
229
+ params, self._deferred_positionals = params[:split_index], params[split_index:]
230
+
231
+ parent = self.parent
232
+ if parent and parent._deferred_positionals:
233
+ self.positionals = parent._deferred_positionals + params
234
+ else:
235
+ self.positionals = params
223
236
 
224
237
  def _process_options(self, params: Collection[BaseOption]):
225
238
  parent = self.parent
@@ -277,7 +290,7 @@ class CommandParameters:
277
290
  for param in action_flags:
278
291
  if param.func is None:
279
292
  raise ParameterDefinitionError(f'No function was registered for param={param!r}')
280
- grouped_ordered_flags[param.before_main][param.order].append(param)
293
+ grouped_ordered_flags[param.before_main][param.order].append(param) # noqa # PyCharm infers the wrong type
281
294
 
282
295
  found_non_always = False
283
296
  invalid = {}
@@ -449,6 +462,7 @@ class CommandParameters:
449
462
  raise exc
450
463
 
451
464
  def try_env_params(self, ctx: Context) -> Iterator[Option]:
465
+ """Yields Option parameters that have an environment variable configured, and did not have any CLI values."""
452
466
  for param in self.options:
453
467
  try:
454
468
  param.env_var # noqa
@@ -460,7 +474,7 @@ class CommandParameters:
460
474
 
461
475
  def required_check_params(self) -> Iterator[Parameter]:
462
476
  ignore = SubCommand
463
- yield from (p for p in self.positionals if p.required and not p.group and not isinstance(p, ignore))
477
+ yield from (p for p in self.all_positionals if p.required and not p.group and not isinstance(p, ignore))
464
478
  yield from (p for p in self.options if p.required and not p.group)
465
479
  pass_thru = self._pass_thru
466
480
  if pass_thru and pass_thru.required and not pass_thru.group:
@@ -108,11 +108,11 @@ class Command(ABC, metaclass=CommandMeta):
108
108
  cmd_cls = cls
109
109
  with ExitStack() as stack:
110
110
  stack.enter_context(ctx)
111
- sub_cmd = CommandParser.parse_args(ctx)
111
+ sub_cmd = CommandParser.parse_args_and_get_next_cmd(ctx)
112
112
  while sub_cmd:
113
113
  cmd_cls = sub_cmd
114
114
  ctx = stack.enter_context(ctx._sub_context(cmd_cls))
115
- sub_cmd = CommandParser.parse_args(ctx)
115
+ sub_cmd = CommandParser.parse_args_and_get_next_cmd(ctx)
116
116
 
117
117
  return cmd_cls()
118
118
 
@@ -24,6 +24,7 @@ __all__ = [
24
24
  'OptionNameMode',
25
25
  'SubcommandAliasHelpMode',
26
26
  'AmbiguousComboMode',
27
+ 'AllowLeadingDash',
27
28
  'DEFAULT_CONFIG',
28
29
  ]
29
30
 
@@ -178,6 +179,36 @@ class AmbiguousComboMode(MissingMixin, Enum):
178
179
  STRICT = 'strict' # Reject multi-char short options that overlap with a single char one before parsing
179
180
 
180
181
 
182
+ class AllowLeadingDash(Enum):
183
+ """
184
+ How a given Parameter should handle values with a leading dash (``-``). Only configurable at the Parameter level,
185
+ not the Command level.
186
+
187
+ The behavior based on each supported option:
188
+
189
+ :NUMERIC: Allow numeric values like ``-5`` and ``-1.3``, but reject values like ``-d``.
190
+ :ALWAYS: Always allow values with a leading dash.
191
+ :NEVER: Never allow values with a leading dash.
192
+ """
193
+
194
+ NUMERIC = 'numeric' # Allow a leading dash when the value is numeric
195
+ ALWAYS = 'always' # Always allow a leading dash
196
+ NEVER = 'never' # Never allow a leading dash
197
+
198
+ @classmethod
199
+ def _missing_(cls, value):
200
+ if isinstance(value, str):
201
+ try:
202
+ return cls._member_map_[value.upper()] # noqa
203
+ except KeyError:
204
+ pass
205
+ elif value is True:
206
+ return cls.ALWAYS
207
+ elif value is False:
208
+ return cls.NEVER
209
+ return super()._missing_(value) # noqa
210
+
211
+
181
212
  # endregion
182
213
 
183
214
 
@@ -32,31 +32,12 @@ if TYPE_CHECKING:
32
32
  from .parameters import Parameter, ActionFlag
33
33
  from .typing import Bool, ParamOrGroup, CommandType, AnyConfig, OptStr, PathLike
34
34
 
35
- __all__ = [
36
- 'Context',
37
- 'ctx',
38
- 'get_current_context',
39
- 'get_or_create_context',
40
- 'get_context',
41
- 'get_parsed',
42
- 'get_raw_arg',
43
- 'ParseState',
44
- ]
35
+ __all__ = ['Context', 'ctx', 'get_current_context', 'get_or_create_context', 'get_context', 'get_parsed', 'get_raw_arg']
45
36
 
46
37
  _context_stack = ContextVar('cli_command_parser.context.stack', default=[])
47
38
  _TERMINAL = Terminal()
48
39
 
49
40
 
50
- class ParseState(Enum):
51
- INITIAL = 1
52
- COMPLETE = 2
53
- FAILED = 3
54
-
55
- @property
56
- def done(self) -> bool:
57
- return self._value_ > 1
58
-
59
-
60
41
  class Context(AbstractContextManager): # Extending AbstractContextManager to make PyCharm's type checker happy
61
42
  """
62
43
  The parsing context.
@@ -69,6 +50,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
69
50
  prog: OptStr = None
70
51
  _terminal_width: Optional[int]
71
52
  allow_argv_prog: Bool = True
53
+ _provided: Dict[ParamOrGroup, int]
72
54
 
73
55
  def __init__(
74
56
  self,
@@ -82,7 +64,6 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
82
64
  ):
83
65
  self.command = command
84
66
  self.parent = parent
85
- self.state = ParseState.INITIAL
86
67
  self.config = _normalize_config(config, kwargs, parent, command)
87
68
  if parent is not None:
88
69
  self._set_argv(parent.prog, argv)
@@ -128,8 +109,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
128
109
  return self.__class__(argv, command, parent=self, **kwargs)
129
110
 
130
111
  def __repr__(self) -> str:
131
- cmd_name = getattr(self.command, '__name__', None)
132
- return f'<{self.__class__.__name__}[state={self.state}, command={cmd_name}]>'
112
+ return f'<{self.__class__.__name__}[command={getattr(self.command, "__name__", None)}]>'
133
113
 
134
114
  def __enter__(self) -> Context:
135
115
  _context_stack.get().append(self)
@@ -182,7 +162,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
182
162
 
183
163
  params = self.params
184
164
  if params:
185
- for group in (params.positionals, params.options, (params.pass_thru,)):
165
+ for group in (params.all_positionals, params.options, (params.pass_thru,)):
186
166
  for param in group:
187
167
  if param and param not in exclude:
188
168
  try:
@@ -220,7 +200,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
220
200
  try:
221
201
  return self._parsed[param]
222
202
  except KeyError:
223
- self._parsed[param] = value = param._init_value_factory(self.state)
203
+ self._parsed[param] = value = param._init_value_factory()
224
204
  return value
225
205
 
226
206
  def set_parsed_value(self, param: Parameter, value: Any):
@@ -237,6 +217,9 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
237
217
  """Not intended to be called by users. Used by Parameters during parsing to handle nargs."""
238
218
  return self._provided[param]
239
219
 
220
+ def get_missing(self) -> List[Parameter]:
221
+ return [p for p in self.params.required_check_params() if self._provided[p] == 0]
222
+
240
223
  # endregion
241
224
 
242
225
  # region Actions
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import keyword
4
4
  import logging
5
5
  from abc import ABC, abstractmethod
6
- from ast import literal_eval, Attribute, Name, GeneratorExp, Subscript, DictComp, ListComp, SetComp
6
+ from ast import literal_eval, Attribute, Name, GeneratorExp, Subscript, DictComp, ListComp, SetComp, Constant, Str
7
7
  from dataclasses import dataclass, fields
8
8
  from itertools import count
9
9
  from typing import TYPE_CHECKING, Union, Optional, Iterator, Iterable, Type, TypeVar, Generic, List, Tuple
@@ -31,13 +31,16 @@ def convert_script(script: Script, add_methods: bool = False) -> str:
31
31
 
32
32
  class Converter(Generic[AC], ABC):
33
33
  converts: Type[AC] = None
34
+ newline_between_members: bool = False
34
35
  _ac_converter_map = {}
35
36
 
36
- def __init_subclass__(cls, converts: Type[AC] = None, **kwargs):
37
+ def __init_subclass__(cls, converts: Type[AC] = None, newline_between_members: bool = None, **kwargs):
37
38
  super().__init_subclass__(**kwargs)
38
39
  if converts:
39
40
  cls.converts = converts
40
41
  cls._ac_converter_map[converts] = cls
42
+ if newline_between_members is not None:
43
+ cls.newline_between_members = newline_between_members
41
44
 
42
45
  def __init__(self, ast_obj: Union[AC, Script], parent: Optional[Converter] = None):
43
46
  self.ast_obj = ast_obj
@@ -90,7 +93,10 @@ class ConverterGroup(Generic[C]):
90
93
  yield from self.members
91
94
 
92
95
  def format_all(self, indent: int = 0) -> Iterator[str]:
93
- for member in self.members:
96
+ newline_between_members = self.member_type.newline_between_members
97
+ for i, member in enumerate(self.members):
98
+ if i and newline_between_members:
99
+ yield ''
94
100
  yield from member.format_lines(indent)
95
101
 
96
102
 
@@ -284,7 +290,7 @@ class ParserConverter(CollectionConverter[AstArgumentParser], converts=AstArgume
284
290
  # endregion
285
291
 
286
292
 
287
- class GroupConverter(CollectionConverter[ArgGroup], converts=ArgGroup):
293
+ class GroupConverter(CollectionConverter[ArgGroup], converts=ArgGroup, newline_between_members=True):
288
294
  ast_obj: ArgGroup
289
295
 
290
296
  def format_lines(self, indent: int = 4) -> Iterator[str]:
@@ -364,6 +370,10 @@ class ParamConverter(Converter[ParserArg], converts=ParserArg):
364
370
  return next(name for name in self._attr_name_candidates() if name not in RESERVED)
365
371
 
366
372
  def _attr_name_candidates(self) -> Iterator[str]:
373
+ dest = self.ast_obj.init_func_raw_kwargs.get('dest')
374
+ if dest is not None and isinstance(dest, (Constant, Str)): # Str is for 3.7 compatibility
375
+ yield getattr(dest, dest._fields[0]) # .value for Constant, .s for Str
376
+
367
377
  long, short, plain = self._grouped_opt_strs
368
378
  if self.is_positional or self.is_pass_thru:
369
379
  yield from plain
@@ -439,6 +449,7 @@ class ParamConverter(Converter[ParserArg], converts=ParserArg):
439
449
  nargs = self.ast_obj.init_func_kwargs.get('nargs')
440
450
  if not nargs:
441
451
  return False
452
+ # TODO: Refactor to take advantage of new nargs=REMAINDER support
442
453
  return nargs in self.ast_obj.get_tracked_refs('argparse', 'REMAINDER', ())
443
454
 
444
455
  @cached_property
@@ -637,9 +648,16 @@ class FlagArgs(OptionArgs):
637
648
  action = None
638
649
  else:
639
650
  if default == opposite:
640
- default = None
651
+ const = None
652
+ if action == 'store_true':
653
+ default = None
654
+ elif not default and action == 'store_false':
655
+ default = 'True'
656
+ const = None
657
+ else:
658
+ const = value if default else None
659
+
641
660
  action = None
642
- const = value if default else None
643
661
 
644
662
  kwargs['type'] = kwargs['nargs'] = None
645
663
  if action:
@@ -137,20 +137,22 @@ class ScriptVisitor(NodeVisitor):
137
137
  # endregion
138
138
 
139
139
  def resolve_ref(self, name: Union[str, AST, Attribute, Name, expr]):
140
- if not isinstance(name, str):
141
- name = get_name_repr(name)
142
- try:
143
- return self.scopes[name]
144
- except KeyError:
145
- pass
146
- try:
147
- obj_name, attr = name.rsplit('.', 1)
148
- except ValueError:
149
- return None
150
- try:
151
- obj = self.scopes[obj_name]
152
- except KeyError:
153
- return None
140
+ if isinstance(name, Attribute) and isinstance(name.value, Call):
141
+ obj = self.visit_Call(name.value)
142
+ attr = name.attr
143
+ else:
144
+ if not isinstance(name, str):
145
+ name = get_name_repr(name)
146
+ try:
147
+ return self.scopes[name]
148
+ except KeyError:
149
+ pass
150
+ try:
151
+ obj_name, attr = name.rsplit('.', 1)
152
+ obj = self.scopes[obj_name]
153
+ except (ValueError, KeyError):
154
+ return None
155
+
154
156
  try:
155
157
  can_call = attr in obj.visit_funcs
156
158
  except (AttributeError, TypeError):
@@ -162,12 +162,7 @@ class CommandMeta(ABCMeta, type):
162
162
  else:
163
163
  return first if include_abc else parent
164
164
 
165
- try:
166
- mro = type.mro(cls)[1:]
167
- except TypeError: # a Command object was provided instead of a Command class
168
- cls = cls.__class__
169
- mro = type.mro(cls)[1:]
170
-
165
+ cls, mro = _mro(cls)
171
166
  first = parent = None
172
167
  for parent_cls in mro:
173
168
  if isinstance(parent_cls, mcs):
@@ -200,6 +195,14 @@ class CommandMeta(ABCMeta, type):
200
195
  return meta
201
196
 
202
197
 
198
+ def _mro(cmd_cls):
199
+ try:
200
+ return cmd_cls, type.mro(cmd_cls)[1:-1] # 0 is always the class itself, -1 is always object
201
+ except TypeError: # a Command object was provided instead of a Command class
202
+ cmd_cls = cmd_cls.__class__
203
+ return cmd_cls, type.mro(cmd_cls)[1:-1]
204
+
205
+
203
206
  def _choice_items(choice: OptStr, choices: Optional[Choices]) -> Sequence[Tuple[OptStr, OptStr]]:
204
207
  if not choices:
205
208
  return ((choice, None),) # noqa
@@ -57,7 +57,7 @@ class CommandHelpFormatter:
57
57
  if meta.usage:
58
58
  return meta.usage
59
59
 
60
- params = self.params.positionals + self.params.options # noqa
60
+ params = self.params.all_positionals + self.params.options # noqa
61
61
  pass_thru = self.params.pass_thru
62
62
  if pass_thru is not None:
63
63
  params.append(pass_thru)
@@ -155,7 +155,7 @@ def get_formatter(command: CommandAny) -> CommandHelpFormatter:
155
155
 
156
156
  def get_usage_sub_cmds(command: CommandCls):
157
157
  cmd_mcs: Type[CommandMeta] = command.__class__ # Using metaclass to avoid potentially overwritten attrs
158
- parent: CommandType = cmd_mcs.parent(command)
158
+ parent: CommandType = cmd_mcs.parent(command, False)
159
159
  if not parent:
160
160
  return []
161
161
 
@@ -405,7 +405,6 @@ class PassThruHelpFormatter(ParamHelpFormatter, param_cls=PassThru):
405
405
 
406
406
  class GroupHelpFormatter(ParamHelpFormatter, param_cls=ParamGroup): # noqa # pylint: disable=W0223
407
407
  required_formatter_map: BoolFormatterMap = {True: '{{{}}}'.format, False: '[{}]'.format}
408
- # TODO: #18 Group order changes between invocations - should be sorted as declared or alphanumerically (config?)
409
408
 
410
409
  def _get_choice_delim(self) -> str:
411
410
  param: ParamGroup = self.param
@@ -425,16 +424,16 @@ class GroupHelpFormatter(ParamHelpFormatter, param_cls=ParamGroup): # noqa # p
425
424
  if description:
426
425
  return description
427
426
  group = self.param
428
- if not group.description and not group._name:
429
- if ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
430
- return 'Mutually {} options'.format('exclusive' if group.mutually_exclusive else 'dependent')
431
- else:
432
- return 'Optional arguments'
433
- else:
427
+ if group.description or group._name:
434
428
  description = group.description or f'{group.name} options'
435
429
  if ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
436
- description += ' (mutually {})'.format('exclusive' if group.mutually_exclusive else 'dependent')
430
+ description += f' (mutually {"exclusive" if group.mutually_exclusive else "dependent"})'
437
431
  return description
432
+ elif ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
433
+ return f'Mutually {"exclusive" if group.mutually_exclusive else "dependent"} options'
434
+
435
+ adjective = 'Required' if group.required else 'Other' if group.contains_required else 'Optional'
436
+ return f'{adjective} arguments'
438
437
 
439
438
  def _get_spacer(self) -> str:
440
439
  group = self.param