Skip to content

Commit 268ded5

Browse files
committed
Add ability to reverse short and long names, add short name only, change argparse results to be stored in func arg name
1 parent 42b5357 commit 268ded5

File tree

6 files changed

+209
-72
lines changed

6 files changed

+209
-72
lines changed

arguably/_commands.py

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import inspect
66
import re
77
from dataclasses import dataclass, field
8-
from typing import Callable, Any, Union, Optional, List, Dict, Tuple, cast
8+
from typing import Callable, Any, Union, Optional, List, Dict, Tuple, cast, Set
99

1010
from docstring_parser import parse as docparse
1111

@@ -92,25 +92,45 @@ def _process(self) -> Command:
9292
assert len(tuple_modifiers) == 1
9393
expected_metavars = len(tuple_modifiers[0].tuple_arg)
9494

95+
# What kind of argument is this? Is it required-positional, optional-positional, or an option?
96+
if param.kind == param.KEYWORD_ONLY:
97+
input_method = InputMethod.OPTION
98+
elif arg_default is util.NoDefault:
99+
input_method = InputMethod.REQUIRED_POSITIONAL
100+
else:
101+
input_method = InputMethod.OPTIONAL_POSITIONAL
102+
95103
# Get the description
96104
arg_description = ""
97105
if docs is not None and docs.params is not None:
98106
ds_matches = [ds_p for ds_p in docs.params if ds_p.arg_name.lstrip("*") == param.name]
99107
if len(ds_matches) > 1:
100108
raise util.ArguablyException(
101-
f"Function parameter `{param.name}` in " f"`{processed_name}` has multiple docstring entries."
109+
f"Function argument `{param.name}` in " f"`{processed_name}` has multiple docstring entries."
102110
)
103111
if len(ds_matches) == 1:
104112
ds_info = ds_matches[0]
105113
arg_description = "" if ds_info.description is None else ds_info.description
106114

107115
# Extract the alias
108116
arg_alias = None
109-
if alias_match := re.match(r"^\[-([a-zA-Z0-9])(/--([a-zA-Z0-9]+))?]", arg_description):
110-
arg_alias, desc_name = alias_match.group(1), alias_match.group(3)
111-
arg_description = arg_description[len(alias_match.group(0)) :].lstrip(" ")
112-
if desc_name is not None:
113-
cli_arg_name = desc_name
117+
has_long_name = True
118+
if input_method == InputMethod.OPTION:
119+
arg_description, arg_alias, long_name = util.parse_short_and_long_name(
120+
cli_arg_name, arg_description, func
121+
)
122+
if long_name is None:
123+
has_long_name = False
124+
else:
125+
cli_arg_name = long_name
126+
else:
127+
if arg_description.startswith("["):
128+
util.warn(
129+
f"Function argument `{param.name}` in `{processed_name}` is a positional argument, but starts "
130+
f"with a `[`, which is used to specify --option names. To make this argument an --option, make "
131+
f"it into be a keyword-only argument.",
132+
func,
133+
)
114134

115135
# Extract the metavars
116136
metavars = None
@@ -121,33 +141,25 @@ def _process(self) -> Command:
121141
if is_variadic:
122142
if len(match_items) != 1:
123143
raise util.ArguablyException(
124-
f"Function parameter `{param.name}` in `{processed_name}` should only have one item in "
144+
f"Function argument `{param.name}` in `{processed_name}` should only have one item in "
125145
f"its metavar descriptor, but found {len(match_items)}: {','.join(match_items)}."
126146
)
127147
elif len(match_items) != expected_metavars:
128148
if len(match_items) == 1:
129149
match_items *= expected_metavars
130150
else:
131151
raise util.ArguablyException(
132-
f"Function parameter `{param.name}` in `{processed_name}` takes {expected_metavars} "
152+
f"Function argument `{param.name}` in `{processed_name}` takes {expected_metavars} "
133153
f"items, but metavar descriptor has {len(match_items)}: {','.join(match_items)}."
134154
)
135155
metavars = [i.upper() for i in match_items]
136156
arg_description = "".join(metavar_split) # Strips { and } from metavars for description
137157
if len(metavar_split) > 3:
138158
raise util.ArguablyException(
139-
f"Function parameter `{param.name}` in `{processed_name}` has multiple metavar sequences - "
159+
f"Function argument `{param.name}` in `{processed_name}` has multiple metavar sequences - "
140160
f"these are denoted like {{A, B, C}}. There should be only one."
141161
)
142162

143-
# What kind of argument is this? Is it required-positional, optional-positional, or an option?
144-
if param.kind == param.KEYWORD_ONLY:
145-
input_method = InputMethod.OPTION
146-
elif arg_default is util.NoDefault:
147-
input_method = InputMethod.REQUIRED_POSITIONAL
148-
else:
149-
input_method = InputMethod.OPTIONAL_POSITIONAL
150-
151163
# Check modifiers
152164
for modifier in modifiers:
153165
modifier.check_valid(arg_value_type, param, processed_name)
@@ -162,6 +174,7 @@ def _process(self) -> Command:
162174
arg_value_type,
163175
arg_description,
164176
arg_alias,
177+
has_long_name,
165178
metavars,
166179
arg_default,
167180
modifiers,
@@ -202,19 +215,24 @@ class CommandArg:
202215

203216
description: str
204217
alias: Optional[str] = None
218+
has_long_name: bool = True
205219
metavars: Optional[List[str]] = None
206220

207221
default: Any = util.NoDefault
208222

209223
modifiers: List[mods.CommandArgModifier] = field(default_factory=list)
210224

225+
def __post_init__(self) -> None:
226+
if not self.has_long_name and self.alias is None:
227+
raise ValueError("CommandArg has no short or long name")
228+
211229
def get_options(self) -> Union[Tuple[()], Tuple[str], Tuple[str, str]]:
212-
if self.input_method is not InputMethod.OPTION:
213-
return cast(Tuple[()], tuple())
214-
elif self.alias is None:
215-
return (f"--{self.cli_arg_name}",)
216-
else:
217-
return f"-{self.alias}", f"--{self.cli_arg_name}"
230+
options = list()
231+
if self.alias is not None:
232+
options.append(f"-{self.alias}")
233+
if self.has_long_name:
234+
options.append(f"--{self.cli_arg_name}")
235+
return cast(Union[Tuple[()], Tuple[str], Tuple[str, str]], tuple(options))
218236

219237
@staticmethod
220238
def _normalize_type_union(
@@ -230,7 +248,7 @@ def _normalize_type_union(
230248
filtered_types = [x for x in util.get_args(value_type) if x is not type(None)]
231249
if len(filtered_types) != 1:
232250
raise util.ArguablyException(
233-
f"Function parameter `{param.name}` in `{function_name}` is an unsupported type. It must be either "
251+
f"Function argument `{param.name}` in `{function_name}` is an unsupported type. It must be either "
234252
f"a single, non-generic type or a Union with None."
235253
)
236254
value_type = filtered_types[0]
@@ -272,15 +290,13 @@ def normalize_type(
272290
if util.get_origin(value_type) == util.Annotated:
273291
type_args = util.get_args(value_type)
274292
if len(type_args) == 0:
275-
raise util.ArguablyException(
276-
f"Function parameter `{param.name}` is Annotated, but no type is specified"
277-
)
293+
raise util.ArguablyException(f"Function argument `{param.name}` is Annotated, but no type is specified")
278294
else:
279295
value_type = type_args[0]
280296
for type_arg in type_args[1:]:
281297
if not isinstance(type_arg, mods.CommandArgModifier):
282298
raise util.ArguablyException(
283-
f"Function parameter `{param.name}` has an invalid annotation value: {type_arg}"
299+
f"Function argument `{param.name}` has an invalid annotation value: {type_arg}"
284300
)
285301
modifiers.append(type_arg)
286302

@@ -297,7 +313,7 @@ def normalize_type(
297313
value_type = str
298314
elif len(type_args) > 1:
299315
raise util.ArguablyException(
300-
f"Function parameter `{param.name}` in `{function_name}` has too many items passed to List[...]."
316+
f"Function argument `{param.name}` in `{function_name}` has too many items passed to List[...]."
301317
f"There should be exactly one item between the square brackets."
302318
)
303319
else:
@@ -308,30 +324,30 @@ def normalize_type(
308324
):
309325
if param.kind in [param.VAR_KEYWORD, param.VAR_POSITIONAL]:
310326
raise util.ArguablyException(
311-
f"Function parameter `{param.name}` in `{function_name}` is an *args or **kwargs, which should "
327+
f"Function argument `{param.name}` in `{function_name}` is an *args or **kwargs, which should "
312328
f"be annotated with what only one of its items should be."
313329
)
314330
type_args = util.get_args(value_type)
315331
if len(type_args) == 0:
316332
raise util.ArguablyException(
317-
f"Function parameter `{param.name}` in `{function_name}` is a tuple but doesn't specify the "
333+
f"Function argument `{param.name}` in `{function_name}` is a tuple but doesn't specify the "
318334
f"type of its items, which arguably requires."
319335
)
320336
if type_args[-1] is Ellipsis:
321337
raise util.ArguablyException(
322-
f"Function parameter `{param.name}` in `{function_name}` is a variable-length tuple, which is "
338+
f"Function argument `{param.name}` in `{function_name}` is a variable-length tuple, which is "
323339
f"not supported."
324340
)
325341
value_type = type(None)
326342
modifiers.append(mods.TupleModifier(list(type_args)))
327343
elif origin is not None:
328344
if param.kind in [param.VAR_KEYWORD, param.VAR_POSITIONAL]:
329345
raise util.ArguablyException(
330-
f"Function parameter `{param.name}` in `{function_name}` is an *args or **kwargs, which should "
346+
f"Function argument `{param.name}` in `{function_name}` is an *args or **kwargs, which should "
331347
f"be annotated with what only one of its items should be."
332348
)
333349
raise util.ArguablyException(
334-
f"Function parameter `{param.name}` in `{function_name}` is a generic type "
350+
f"Function argument `{param.name}` in `{function_name}` is a generic type "
335351
f"(`{util.get_origin(value_type)}`), which is not supported."
336352
)
337353

@@ -349,36 +365,38 @@ class Command:
349365
alias: Optional[str] = None
350366
add_help: bool = True
351367

352-
arg_map: Dict[str, CommandArg] = field(init=False)
368+
func_arg_names: Set[str] = field(default_factory=set)
369+
cli_arg_map: Dict[str, CommandArg] = field(default_factory=dict)
353370

354371
def __post_init__(self) -> None:
355-
self.arg_map = dict()
372+
self.cli_arg_map = dict()
356373
for arg in self.args:
357-
assert arg.func_arg_name not in self.arg_map
358-
if arg.cli_arg_name in self.arg_map:
374+
assert arg.func_arg_name not in self.func_arg_names
375+
self.func_arg_names.add(arg.func_arg_name)
376+
377+
if arg.cli_arg_name in self.cli_arg_map:
359378
raise util.ArguablyException(
360-
f"Function parameter `{arg.func_arg_name}` in `{self.name}` conflicts with "
361-
f"`{self.arg_map[arg.cli_arg_name].func_arg_name}`, both names simplify to `{arg.cli_arg_name}`"
379+
f"Function argument `{arg.func_arg_name}` in `{self.name}` conflicts with "
380+
f"`{self.cli_arg_map[arg.cli_arg_name].func_arg_name}`, both have the CLI name `{arg.cli_arg_name}`"
362381
)
363-
self.arg_map[arg.cli_arg_name] = arg
364-
self.arg_map[arg.func_arg_name] = arg
382+
self.cli_arg_map[arg.cli_arg_name] = arg
365383

366384
def call(self, parsed_args: Dict[str, Any]) -> Any:
367385
"""Filters arguments from argparse to only include the ones used by this command, then calls it"""
368386

369387
args = list()
370388
kwargs = dict()
371389

372-
filtered_args = {k: v for k, v in parsed_args.items() if k in self.arg_map}
390+
filtered_args = {k: v for k, v in parsed_args.items() if k in self.func_arg_names}
373391

374392
# Add to either args or kwargs
375393
for arg in self.args:
376394
if arg.input_method.is_positional and not arg.is_variadic:
377-
args.append(filtered_args[arg.cli_arg_name])
395+
args.append(filtered_args[arg.func_arg_name])
378396
elif arg.input_method.is_positional and arg.is_variadic:
379-
args.extend(filtered_args[arg.cli_arg_name])
397+
args.extend(filtered_args[arg.func_arg_name])
380398
else:
381-
kwargs[arg.func_arg_name] = filtered_args[arg.cli_arg_name]
399+
kwargs[arg.func_arg_name] = filtered_args[arg.func_arg_name]
382400

383401
# Call the function
384402
if util.is_async_callable(self.function):

arguably/_context.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
get_ancestors,
2222
get_parser_name,
2323
warn,
24-
func_info,
24+
func_or_class_info,
2525
)
2626

2727

@@ -246,7 +246,7 @@ def _validate_args(self, cmd: Command, is_root_cmd: bool) -> None:
246246
if issubclass(arg_.arg_value_type, bool):
247247
if arg_.input_method is not InputMethod.OPTION or arg_.default is NoDefault:
248248
raise ArguablyException(
249-
f"Function parameter `{arg_.func_arg_name}` in `{cmd.name}` is a `bool`. Boolean parameters "
249+
f"Function argument `{arg_.func_arg_name}` in `{cmd.name}` is a `bool`. Boolean parameters "
250250
f"must have a default value and be an optional, not a positional, argument."
251251
)
252252

@@ -275,7 +275,7 @@ def _set_up_args(self, cmd: Command) -> None:
275275
continue
276276

277277
# Optional kwargs for parser.add_argument
278-
add_arg_kwargs: Dict[str, Any] = dict(type=arg_.arg_value_type)
278+
add_arg_kwargs: Dict[str, Any] = dict(type=arg_.arg_value_type, action="store")
279279

280280
arg_description = arg_.description
281281
description_extras = []
@@ -327,12 +327,12 @@ def _set_up_args(self, cmd: Command) -> None:
327327
mapping = self.set_up_enum(arg_.arg_value_type)
328328
add_arg_kwargs.update(choices=[n for n in mapping])
329329

330-
cli_arg_names: Tuple[str, ...] = (arg_.cli_arg_name,)
330+
name_spec: Tuple[str, ...] = (arg_.func_arg_name,)
331331

332332
# Special handling for optional arguments
333333
if arg_.input_method is InputMethod.OPTION:
334-
cli_arg_names = arg_.get_options()
335-
add_arg_kwargs.update(dest=arg_.cli_arg_name)
334+
name_spec = arg_.get_options()
335+
add_arg_kwargs.update(dest=arg_.func_arg_name)
336336

337337
# `bool` should be flags
338338
if issubclass(arg_.arg_value_type, bool):
@@ -352,13 +352,23 @@ def _set_up_args(self, cmd: Command) -> None:
352352
for modifier in arg_.modifiers:
353353
modifier.modify_arg_dict(cmd, arg_, add_arg_kwargs)
354354

355+
if (
356+
"choices" not in add_arg_kwargs
357+
and "metavar" not in add_arg_kwargs
358+
and add_arg_kwargs["action"] not in ["store_true", "store_false", "count", "help", "version"]
359+
):
360+
if arg_.input_method is InputMethod.OPTION:
361+
add_arg_kwargs.update(metavar=arg_.cli_arg_name.upper())
362+
else:
363+
add_arg_kwargs.update(metavar=arg_.cli_arg_name)
364+
355365
# Add the argument to the parser
356366
argspec = log_args(
357367
logger.debug,
358368
f"Parser({repr(get_parser_name(parser.prog))}).",
359369
parser.add_argument.__name__,
360370
# Args for the call are below:
361-
*cli_arg_names,
371+
*name_spec,
362372
**add_arg_kwargs,
363373
)
364374
parser.add_argument(*argspec.args, **argspec.kwargs)
@@ -461,7 +471,7 @@ def high_five(*people):
461471
def _soft_failure(self, msg: str, function: Optional[Callable] = None) -> None:
462472
if self._options.strict:
463473
if function is not None:
464-
info = func_info(function)
474+
info = func_or_class_info(function)
465475
if info is not None:
466476
source_file, source_file_line = info
467477
msg = f"({source_file}:{source_file_line}) {function.__name__}: {msg}"

0 commit comments

Comments
 (0)