55import inspect
66import re
77from 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
1010from 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 ):
0 commit comments